mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1029 Commits
v0.0.0
...
code-cover
Author | SHA1 | Date | |
---|---|---|---|
27363d34cc | |||
7b3a8b4ecf | |||
a8bd5d332a | |||
0462574d8d | |||
1325fb8c9a | |||
cf42dca777 | |||
afd97bd304 | |||
45ce442cf2 | |||
e8c5c9e9ef | |||
caac6855da | |||
e379900526 | |||
0bc1e7057f | |||
d94a74dfee | |||
a25a52c21b | |||
268bc36843 | |||
8e7eaaae24 | |||
b69ba0b617 | |||
1bedf937f8 | |||
74a521a271 | |||
6376ac28d5 | |||
2222a90884 | |||
09f04be77d | |||
636ecaf4e0 | |||
07fa504c78 | |||
5f74023bb8 | |||
b31e253fa4 | |||
f75f00e4d0 | |||
13deb83517 | |||
62b7d2fd1a | |||
aa5062ea6f | |||
394f2e0999 | |||
4a967b126b | |||
5f780a0947 | |||
0671ee0bef | |||
55b2b5a467 | |||
f7a4160c3f | |||
294290908b | |||
8ed4f547e0 | |||
3393e797d0 | |||
7de059b95e | |||
c2b0091f86 | |||
d96ad93e1e | |||
e0373e6f19 | |||
f50f169ff0 | |||
eada1ab87e | |||
51d4ffc5d9 | |||
59a7b9d12b | |||
3bb5ca50b2 | |||
353ee4a576 | |||
eaffc2574f | |||
5142274e9d | |||
a05b592f00 | |||
e7c89cf77c | |||
09444f0cff | |||
f329acc2ec | |||
ee3a0cc630 | |||
b40498786a | |||
43f551dd2e | |||
1f4382bea8 | |||
96bc8a829e | |||
0cdf4d95e5 | |||
a6ac0ee17b | |||
1b943bdf7d | |||
f831bb4645 | |||
82c486b202 | |||
02b888f7c1 | |||
69c97fed09 | |||
05cb89725e | |||
43ae9b672c | |||
27270dd589 | |||
0c5ed7adfb | |||
4280a3cd4a | |||
b8944ba65c | |||
a8fcd85f1a | |||
cc45d872c7 | |||
e0e76a1aa8 | |||
d9b417e9e5 | |||
101a4bc209 | |||
449461afae | |||
1987206b94 | |||
7dd33adfd1 | |||
63fe3d41cc | |||
59e4eb5143 | |||
78e627a471 | |||
3adc06d11b | |||
85fc9daa8b | |||
d463a2f0e5 | |||
863eae42c5 | |||
4b373d5ed4 | |||
4077da1491 | |||
3718473f05 | |||
54bcbb5d91 | |||
408f739e2d | |||
f481d5edae | |||
cff81ae86d | |||
f9d6a0ee72 | |||
9ea999f15f | |||
eac229ab7c | |||
7a865b2e15 | |||
32c588db55 | |||
aa670ad6f1 | |||
c1ba73a2da | |||
606b6c88ab | |||
a6f329750c | |||
c07b28e694 | |||
b3bef9e556 | |||
2ff427fb90 | |||
3d89b0c7a1 | |||
90db52db47 | |||
87004621ce | |||
de8089b0bb | |||
ae691391b6 | |||
a64b36fdb9 | |||
db5c473952 | |||
2b0bff8f16 | |||
2e7f606667 | |||
73e9f801e2 | |||
9a40f20004 | |||
6631fb5a69 | |||
f76ca1804a | |||
d25ad328f3 | |||
dfbcdc2c1b | |||
d484c957bb | |||
70714b6feb | |||
c9db5fd856 | |||
d54a709e7c | |||
09f20ecc1c | |||
b8b0221ce6 | |||
596d1bdc21 | |||
fb1dce9dbb | |||
fedfb603f6 | |||
8478b95e45 | |||
7bb2741a5a | |||
693a438d42 | |||
2a8e68cec2 | |||
e3435c66df | |||
192b751e57 | |||
d9643eb59e | |||
19c3178062 | |||
516fa90a20 | |||
56aabad8ad | |||
4a80beac35 | |||
cdedc58ec1 | |||
ed084edc48 | |||
b435a0e7ac | |||
731db63e78 | |||
2ee2bc8b02 | |||
b4c99dc03a | |||
730c09989d | |||
9e0733a143 | |||
8dd1106a44 | |||
4d389bb6cc | |||
6d6b3cb1a3 | |||
e84482fbbe | |||
f1d9c29786 | |||
34f4c1f6cc | |||
2390ddc02d | |||
70561705e5 | |||
05d58d8248 | |||
13610ef814 | |||
9c49890399 | |||
b9884ec545 | |||
ebd8f348ca | |||
90c4cbf2ae | |||
987e16cad0 | |||
15dfdc2229 | |||
35110e0610 | |||
bf722f61c7 | |||
08e8151b1f | |||
9977451b07 | |||
9f610a521e | |||
67a4e88e44 | |||
f0bd2f197d | |||
5fe908bbbf | |||
f9fc8fd2e8 | |||
9ea5198bda | |||
35732515ac | |||
3ea905dc68 | |||
062fadfa49 | |||
58983670f1 | |||
18fadf5634 | |||
80917b7198 | |||
f21dc3e2f4 | |||
d92e076829 | |||
cd491a7935 | |||
90269525be | |||
225408c57d | |||
8cfacc9cbc | |||
9a270539c6 | |||
df22d42412 | |||
49a7eb30c0 | |||
e41ea6fb1a | |||
636ec2a8f2 | |||
9eae9dcee3 | |||
6c26e1235c | |||
bacca5383a | |||
32e72c832f | |||
05aaed07b2 | |||
7c750f9e43 | |||
55bf4dc0f0 | |||
0afbd6f17a | |||
66b997d98c | |||
d485fd00a0 | |||
3322faeeb2 | |||
c32d894e97 | |||
ad737b8e02 | |||
bcc86fbcb6 | |||
d5c7527f8d | |||
5df65f67c3 | |||
79e65025cb | |||
dff6dafe85 | |||
adcc862acb | |||
8bf884d425 | |||
a6b282598b | |||
77089e31e4 | |||
7c6bae491f | |||
d5586e12d4 | |||
7e1f4791ed | |||
9d5ecb5f9c | |||
236dce37da | |||
a2ee48f2fb | |||
b74a0d1141 | |||
34d7b84d1e | |||
d999b901bb | |||
4a9d133c11 | |||
3a7604368a | |||
df2e611c42 | |||
30e48b658f | |||
f095ac169a | |||
f0030a1891 | |||
1d3af3245a | |||
f4612116b9 | |||
65ab3008e6 | |||
36026d8556 | |||
436ad60f4e | |||
e0ee333717 | |||
3b09c5ac91 | |||
6994499f14 | |||
9e19b5875b | |||
c3e5147a19 | |||
5e46815ac7 | |||
7b37c60e11 | |||
cf9fdbe6e4 | |||
66dfddc96d | |||
1fa2347608 | |||
6bed43fe1f | |||
46806c8377 | |||
cf93438df1 | |||
ea43130135 | |||
5e4daf8c3d | |||
20474aeb55 | |||
825213c02a | |||
13cef01374 | |||
7271f90dc6 | |||
8b5ebe0645 | |||
24e49bf131 | |||
5a0ef354f1 | |||
f8a9c10a77 | |||
bace8b5c1e | |||
d8c942c77e | |||
7fccd4a35e | |||
b78dd26f23 | |||
47c63e0e1d | |||
5d137f3c20 | |||
2bbdfda92e | |||
2f2f16267f | |||
051268168a | |||
2b264df41b | |||
eba193278e | |||
462b41b4ea | |||
ab5815c277 | |||
f75ffb53f5 | |||
3387f8f656 | |||
e8325e8268 | |||
c2491497b5 | |||
4596005a1f | |||
8d9cbbb526 | |||
17e51799f5 | |||
df797cdc2c | |||
57fc26c0f7 | |||
abc2cfd14c | |||
612fefcc65 | |||
0bbe376e41 | |||
04223dbc58 | |||
b5c4348de1 | |||
751749325f | |||
1e2e48b149 | |||
01b454351b | |||
6696b1b5f7 | |||
5da307f02e | |||
b391c96e64 | |||
0ee73f5164 | |||
1a1861bfcb | |||
717c43f10b | |||
dafbdd5f34 | |||
c204dbd6cd | |||
6e7f51758e | |||
ea89a6f221 | |||
94c9e13106 | |||
64c2f00d2a | |||
8f8d6b5f08 | |||
c49a1d3fbf | |||
abf573fa47 | |||
13e3e5e8ea | |||
faa558b432 | |||
4eea5e822b | |||
fe3e348128 | |||
7535b7110d | |||
0aaf6dff41 | |||
aca58743ea | |||
f6a262bcde | |||
9cc81866eb | |||
0607c2f231 | |||
fe0ba62026 | |||
f2af7b2588 | |||
e145540132 | |||
d21b84956c | |||
841ce9fd68 | |||
a4efc7c9c4 | |||
e6ecb0ec0b | |||
c420d0f63c | |||
f3ef7f6e18 | |||
e36435bb4c | |||
825964fdc6 | |||
5202e137d5 | |||
d23d837b8c | |||
8c537c112d | |||
106b971410 | |||
39cf86ed85 | |||
5de7a50fc0 | |||
ba4301487f | |||
f77156772a | |||
a6d6035725 | |||
5e0d24a1f1 | |||
9434570c2d | |||
674c0ca7b8 | |||
cccc8012c9 | |||
29bfcf7ac5 | |||
2091768c84 | |||
36ab6f5808 | |||
b45e80654c | |||
8c60c9e076 | |||
d47fe0609f | |||
3b5f3c6665 | |||
1bfba85d58 | |||
cb14928866 | |||
4088e4c66e | |||
01a1bcdc8a | |||
05c3d2a5db | |||
7da250914e | |||
eaad5377b4 | |||
9f15ee58df | |||
ee267cd114 | |||
b35a51ef3a | |||
7ce43f4018 | |||
3ba8be02e7 | |||
9a9d3f5c32 | |||
0adaeb5465 | |||
783cab2e50 | |||
a47d2d4454 | |||
1f728050c8 | |||
15ec99577a | |||
fb2e498aa9 | |||
7529a9a2a2 | |||
22b02c261f | |||
8f014710c0 | |||
308d1afb83 | |||
c15276d239 | |||
ecae517de0 | |||
69cc2a0946 | |||
7a8fc14686 | |||
eaa886c31c | |||
20ae2bc57a | |||
96c975d17e | |||
ff8d784582 | |||
53bee83047 | |||
6e343c36a8 | |||
e29c275b4c | |||
7faf363180 | |||
a503cb4757 | |||
b3470ac909 | |||
ba4c93ccf5 | |||
87401ad569 | |||
181afb34f8 | |||
bfdfa28922 | |||
21840d9245 | |||
d9c3332cb2 | |||
29d684f9c3 | |||
a832f47508 | |||
4557cf626f | |||
8c68aaad49 | |||
260c6fccd2 | |||
d40f2eb500 | |||
b6f3975bc1 | |||
f2bd3c0ffb | |||
3ae01fdaa0 | |||
2452b42f81 | |||
3303ac21c9 | |||
1b277bda87 | |||
5f67c243c0 | |||
9bbfb31bf7 | |||
5b805fe1da | |||
24ed25aa37 | |||
2ad0b60aeb | |||
37bd6be77b | |||
88ad591a83 | |||
30c36a26e2 | |||
6d6afc5140 | |||
fc79d89f10 | |||
57fba32fa2 | |||
b41f9b9261 | |||
a9aa7538f3 | |||
1b13d1b969 | |||
e6b09f1b94 | |||
8fa592dff0 | |||
a6d2950260 | |||
b22ddb50f1 | |||
0aa10ba2a1 | |||
56c74af1ff | |||
6460d850ca | |||
b05aa7b385 | |||
97c15208b5 | |||
375464eb1a | |||
811262ed8b | |||
f816cb4ce8 | |||
7b5272e926 | |||
d412d39164 | |||
d41fb72ded | |||
4676396b5f | |||
b97565d2e7 | |||
a697121b97 | |||
12615cd0dc | |||
cba73eec44 | |||
f5ed0648ba | |||
ac5ffeda09 | |||
db25a65753 | |||
296f1ae2de | |||
579cdaa2e2 | |||
69db3c0171 | |||
7c71098d86 | |||
490f833a33 | |||
a81c1971f1 | |||
76034696e9 | |||
454f82883e | |||
6a9a9e1fdb | |||
cf2ad66955 | |||
4cfaf6e666 | |||
bc563ea963 | |||
65ac35a93e | |||
1ee51ca9a7 | |||
e9e15dbbe3 | |||
a665a3d153 | |||
9da5f88ecf | |||
14e9b34636 | |||
e619caea42 | |||
a133fe8c6f | |||
29dd5e71a1 | |||
9b52dec725 | |||
6bc55ea7ab | |||
717affa591 | |||
6a9eeaaba2 | |||
98eaeddbfe | |||
30698c62e3 | |||
79e240f630 | |||
6825ffe416 | |||
b9bf00d329 | |||
5ae7457f45 | |||
d78dfcb1de | |||
95c4c59ca0 | |||
3a06c5df72 | |||
d30144a16a | |||
c0990fe6c4 | |||
af382bfee4 | |||
6705369ca6 | |||
fb05957198 | |||
6b21f5aa5a | |||
0fac8d8d3b | |||
f39193ab61 | |||
a883948d56 | |||
da86d3303f | |||
2f3138208a | |||
e688e10684 | |||
66b62df70b | |||
a4dd45e6a6 | |||
b4439b7d78 | |||
bf566b7bb7 | |||
2c897a1b18 | |||
a5cfbb3181 | |||
119a630643 | |||
c6fe6b5e3e | |||
6ff43b11b1 | |||
f35655028e | |||
709af3e92b | |||
5ec9dce507 | |||
1e81638433 | |||
145b613f5d | |||
a243c3f577 | |||
75b9ba907f | |||
1a368ac4d4 | |||
2833e8ba23 | |||
de3837f777 | |||
8dc5eaca52 | |||
5c41de0c9d | |||
7a258f022f | |||
cbdf1c3ccf | |||
dcab2404fa | |||
05e80094de | |||
9fab994dec | |||
6eddd1ccd3 | |||
211fb824a1 | |||
17c7b97abe | |||
ac3c539d45 | |||
c1e94e661f | |||
8a6ee5905a | |||
36c636474d | |||
0bff122b1c | |||
2ffb9bb574 | |||
6190d00644 | |||
67673a55f7 | |||
4448509d92 | |||
8482961f7f | |||
552f8cc4ef | |||
21b32633c5 | |||
8863132e8e | |||
f72b556d92 | |||
4b7e0a19eb | |||
3db92322ba | |||
aac668dfca | |||
0636dcf19d | |||
3fca56e819 | |||
6124d8c493 | |||
9ef99a3aa9 | |||
66fc47edae | |||
af948e76f3 | |||
dfd98efe8b | |||
a8de833c43 | |||
3bff748fbe | |||
8adf66512b | |||
d4313e6f95 | |||
24fdfc9aef | |||
e5f454bac3 | |||
ca9ce5ed16 | |||
2087622bd6 | |||
a651e27a20 | |||
0fd0b6e1fd | |||
c63569d592 | |||
7b7d00c642 | |||
9e27889f91 | |||
8bbb1966a4 | |||
5d00e295fd | |||
0d4a2b65a0 | |||
2ba53649bd | |||
31f4af721d | |||
ce734ba783 | |||
77196746b3 | |||
99650ab732 | |||
96b7d4a15d | |||
ce1aff1557 | |||
9d5949e9d1 | |||
3ca681a4a6 | |||
49449e42be | |||
dad3167f16 | |||
3cc632fbbb | |||
f780d47198 | |||
e0bd6b9d4e | |||
5cf0f7030d | |||
77b763a88f | |||
f9390ceb10 | |||
bc41ff0540 | |||
54e91eb074 | |||
a42a1a97ba | |||
f3d5e26e1e | |||
99eae385b8 | |||
f6d67b94f3 | |||
2d9bb2130e | |||
10fff8f0f5 | |||
8ee994ce7d | |||
86aed2185d | |||
64ec151e4b | |||
3e4e661b28 | |||
7713ca31e5 | |||
b0da1b4811 | |||
0f302d30ec | |||
140e851fcd | |||
b9b4343fd5 | |||
eff49beb36 | |||
952d74207d | |||
853d146457 | |||
97d6a39677 | |||
095a597381 | |||
a66aec6965 | |||
03a3ef38e1 | |||
464d706920 | |||
61dd5acb80 | |||
1e5cf8655e | |||
f096f4f55f | |||
85442c4634 | |||
a357405f3a | |||
6070ee0f83 | |||
f8721a324d | |||
ca861a6349 | |||
493c6576c3 | |||
a8765af589 | |||
ca8484b466 | |||
7a8d746a54 | |||
1cffcd9b9e | |||
b6c041c9b5 | |||
47b35db03e | |||
78985ff633 | |||
93a5380c09 | |||
6677f1faf5 | |||
e7b32e941a | |||
5abd240d50 | |||
759c2d4a6f | |||
92772952fd | |||
2f53cbf088 | |||
c3b74a869a | |||
841d1927a9 | |||
06c450e9a5 | |||
67de638c76 | |||
c90b8eef97 | |||
6ad4598e7e | |||
fd580083d5 | |||
a0003c5f13 | |||
8291c96e69 | |||
561325e66d | |||
a840f39a91 | |||
64f9b50e40 | |||
0273d2e02c | |||
daf3b980ef | |||
6e36cd139a | |||
9ca76348ff | |||
58a5725ad2 | |||
116c3c787c | |||
74a896b3cd | |||
e2ebac27ea | |||
b799f3a46d | |||
3a3cfbc85e | |||
161b9454c2 | |||
b83b7145af | |||
26c4e466bd | |||
42f752e400 | |||
0b67a40d57 | |||
69ab9788fc | |||
7972187b77 | |||
a809847d60 | |||
203285bab9 | |||
52c7a098cc | |||
75bc937995 | |||
dd41145912 | |||
0983bf02e2 | |||
0deb59b6e2 | |||
fdc9fd67d8 | |||
01eb4b473d | |||
4ff42c11e6 | |||
85d82ab9ca | |||
dc6cde2cf1 | |||
ea1c1bb8d4 | |||
c6eca9f895 | |||
e71f622453 | |||
b6266878d4 | |||
4907fccf48 | |||
f4b84941cd | |||
c2df87bcc6 | |||
79f33c749b | |||
fd316167d8 | |||
f60d857c93 | |||
31628cfdcb | |||
4d42f74c0c | |||
13729ec4b6 | |||
8dc4c0871a | |||
207e080b9e | |||
39fe63fb2d | |||
24c2ffefc9 | |||
c293a1742d | |||
cb46756d31 | |||
332e731023 | |||
f9263975bc | |||
67ebf7aa19 | |||
2b52d5edbc | |||
948e6ea6f8 | |||
ca9247fb19 | |||
73455b4264 | |||
28b0793fc9 | |||
c904726259 | |||
6606b65c9b | |||
61160fd2f5 | |||
bf71f9ea16 | |||
fe751fdb23 | |||
947f91d570 | |||
c5d4e30e24 | |||
f560aa7523 | |||
6bcfb2dd51 | |||
bf062124f7 | |||
221666f59a | |||
4369a2d161 | |||
cd6ee4ef5e | |||
872b17cf24 | |||
88e11347bc | |||
a3dd489c70 | |||
0c1c108b2b | |||
f02ed43f33 | |||
63c3d7ceee | |||
dac45a884e | |||
ec589c2639 | |||
f65e777d1b | |||
684ac9fa24 | |||
330cbc6a68 | |||
14bfca8c3a | |||
20c07d31b2 | |||
64b4f67477 | |||
a8ceadc300 | |||
973d25f467 | |||
0d06701e2f | |||
379f1cc217 | |||
7b7ae4ff89 | |||
8e83a401eb | |||
2d1891a182 | |||
8df066df12 | |||
bd59f95e1a | |||
2b982a1c0c | |||
ab64fbc904 | |||
733b98f072 | |||
7c538a3658 | |||
8298ba5765 | |||
33a23773d8 | |||
21a3b82845 | |||
8688eb5da0 | |||
5b0ea9673f | |||
44fd8adeba | |||
a5e03d55c3 | |||
80629322ea | |||
946efbcb7f | |||
be8a314d2b | |||
0a7203cafe | |||
786fed0151 | |||
9cd8228a20 | |||
652b5f22dd | |||
eed3c06789 | |||
3b283d4a98 | |||
bc6b5ba7b3 | |||
74789ae88f | |||
295d6dee74 | |||
5010a1e312 | |||
3c2f7ea622 | |||
94f02f0ad8 | |||
375f84b24e | |||
06c649dfd0 | |||
71eca70a22 | |||
53c7bc622c | |||
975ae45e49 | |||
e7c68c1a5c | |||
5beeb78220 | |||
c90ba7aa0f | |||
802ccc1b9a | |||
b6ef251625 | |||
fd707d6a07 | |||
392cd8569f | |||
e32eda26d9 | |||
d8aaccf80c | |||
d5fd5f5f2d | |||
2cb69c12f1 | |||
7c75346a1a | |||
148d15b6d9 | |||
a46a79df59 | |||
e350f9b335 | |||
bd00773f1b | |||
ef3c7f0fd6 | |||
f4f44f978e | |||
442416efc3 | |||
ef33ffedcf | |||
430d4aeaa7 | |||
171632f83f | |||
1fa7141b58 | |||
916cc36430 | |||
27b877dd33 | |||
5cbe1c410f | |||
7846af390e | |||
79d9ebc805 | |||
25b853c535 | |||
a93141343f | |||
9a467c5ecd | |||
70be2ae596 | |||
36eb0a108e | |||
0bf6fb1739 | |||
892adf4c47 | |||
5d1d004b72 | |||
dea5a60b2d | |||
652a1b7650 | |||
350843af1e | |||
e04c4a8ee3 | |||
9d0c3f7535 | |||
9561d4da2e | |||
8296dcf946 | |||
e62e8b88c2 | |||
4388a248b9 | |||
f9cf0aaf23 | |||
dc9ee09838 | |||
7cb27283c5 | |||
10a9840b34 | |||
ce3e04bfe8 | |||
52f93f8f12 | |||
af9e1a122d | |||
9017b8ec11 | |||
bf4f687a2a | |||
9d4e6eb825 | |||
fba4afb7d2 | |||
8c74f784f7 | |||
69ca1ffa59 | |||
7d1b00877e | |||
1a48fed1f7 | |||
bc86359e63 | |||
f6822f1502 | |||
398c34d842 | |||
72a893be95 | |||
7b23b0e103 | |||
0ce7878042 | |||
da8483e6a6 | |||
16f70fd946 | |||
78aa898b37 | |||
b7f94a222d | |||
7bea2c26b8 | |||
7c178b8095 | |||
865f085094 | |||
28fe69fe94 | |||
232cf8d426 | |||
22e74983b0 | |||
c88dd2257a | |||
439d8d396f | |||
6d8086c09b | |||
e85f252f29 | |||
4b818ad51c | |||
c2518448a3 | |||
e7a8deed05 | |||
0ac599d20c | |||
7d7074e6b7 | |||
35ca34d07d | |||
90d7316b4c | |||
904b4e96d9 | |||
2c46c59a79 | |||
297ff86895 | |||
a154401424 | |||
ad2713fc00 | |||
6388cfaf40 | |||
167f38e342 | |||
919b3c3435 | |||
2e1ab22173 | |||
0a23563d7e | |||
37e4ec6364 | |||
6a8b947c2e | |||
a16ac37625 | |||
cf4c7826b2 | |||
a0a26f0a1e | |||
a921139a12 | |||
36da7b66c8 | |||
3aa87544eb | |||
6121fa505e | |||
a5ba5befd1 | |||
b7214a306c | |||
d7616e941a | |||
834a2f1e4d | |||
0e5f2fe748 | |||
e0bcb5e0b9 | |||
59d4890eae | |||
51da5360da | |||
2655aef28b | |||
45d3a7a124 | |||
662e4f8940 | |||
c06993cb8e | |||
a650f30ce8 | |||
0a924b2dcb | |||
89f62683ce | |||
143d88f3df | |||
d166a65422 | |||
dd268993b3 | |||
13a35b288f | |||
81e653d31b | |||
875ec8b8bd | |||
989df9b857 | |||
0829d3c176 | |||
ce64889b04 | |||
d3a0bfc5f6 | |||
e965c603d2 | |||
0e2fb8c96c | |||
2db1d84d3c | |||
12a1916007 | |||
b4526e9895 | |||
a2d867c860 | |||
05b1c37379 | |||
906cfe9268 | |||
3c8054faa7 | |||
c6c9046826 | |||
2bbbbf6fdd | |||
9cce4001af | |||
2e944cf2f4 | |||
2b0143775c | |||
49fec7d8f2 | |||
ca1ac2bb83 | |||
50b1a7e6b0 | |||
69ce2c0473 | |||
a3b446dbe7 | |||
1032d9927f | |||
12e8a50abc | |||
a4142097f8 | |||
b388ccb6f3 | |||
e011502b7e | |||
4f167cb836 | |||
9455d438e2 | |||
a356ecf9b6 | |||
066ac591ac | |||
62f006b89a | |||
ee75ff2753 | |||
e4c9defb70 | |||
bb102c1918 | |||
24ebe2946c | |||
ba82b1fa27 | |||
e3b145e7b7 | |||
242c3731ee | |||
5f7eee8eac | |||
1833f6ff0a | |||
e5fb954645 | |||
13f76dc020 | |||
b409bdcc73 | |||
8c3cb3f585 | |||
76a8b4df50 | |||
a03680311d | |||
6ee36cb5c7 | |||
5625326c65 | |||
b912419839 | |||
fe01ead023 | |||
229c105d0c | |||
b6e044345f | |||
d9906121e1 | |||
3e019f7f34 | |||
eb34cb6f27 | |||
3a3178bcb9 | |||
cdf6580ecc | |||
c42bc74f1f | |||
35fd79f577 | |||
4ef0682e5a | |||
d0b7047189 | |||
ae3f936b66 | |||
1ef492809b | |||
5bf9dd3a9d | |||
b18a66f66b | |||
1dadfdc699 | |||
14a3f51b73 | |||
96116aeaec | |||
7fd31b6a64 | |||
299bc0db13 | |||
4b9ccae442 | |||
079ce552e3 | |||
163684e3a9 | |||
f698f561c9 | |||
cb207f18a5 | |||
76a5cdc977 | |||
a82af1d2d1 | |||
ac7d51ad80 | |||
797a739c92 | |||
666b59b463 | |||
a83d9a070c | |||
7637377471 | |||
6515f88d92 | |||
92534b9c82 | |||
c12360daa8 | |||
3d28118f3e | |||
04adfde064 | |||
d8aabfd448 | |||
cf95870d9d | |||
55f8876bcc | |||
9fb66186f0 | |||
da8fe99ca4 | |||
20374fde36 | |||
5131f722a7 | |||
1ef0a1028f | |||
0fd1f04eda | |||
5c0ba5d06c | |||
d9532b6fa0 | |||
b96065514f | |||
0e9b8e4140 | |||
d1c773360f | |||
74538bba8d | |||
64c95e3811 | |||
33fd70291a | |||
0cb4bc951a | |||
3761ab9610 | |||
8c29bba108 | |||
4e41261237 | |||
77529ef3b1 | |||
0ba96adbbc | |||
7df277c0bc | |||
c94f7b10bd | |||
83a76f7d6f | |||
6c988241eb | |||
29145dfc2d | |||
4b74e8ec70 | |||
612012aff8 | |||
6ab60d0ccd | |||
6daed83d88 | |||
f25442c036 | |||
ffffd447f2 | |||
4b511c47f0 | |||
158d471a98 | |||
107a90395c | |||
ce5fd53822 | |||
810ca78215 | |||
eb945b3315 | |||
34f24fe331 | |||
743392017d | |||
15b877f005 | |||
0653769156 | |||
3ed319872a | |||
ee124671d8 | |||
1b4dabd37c | |||
fdd253f042 | |||
1a15fdd2f0 | |||
2c66280b3f | |||
778c39d947 | |||
fa15addfb2 | |||
afbb9474b7 | |||
0acb4f8cb1 | |||
08de0938a0 | |||
2c9b80c177 | |||
e8c19df8c9 | |||
7681003512 | |||
dba8db19cb | |||
d199cdf088 | |||
f2840c5ca4 | |||
1c7a0ba4e1 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @CameronDiver @hedss @pdcastro @srlowe @thgreasi
|
18
.github/ISSUE_TEMPLATE.md
vendored
18
.github/ISSUE_TEMPLATE.md
vendored
@ -1,2 +1,16 @@
|
||||
- **resin-cli version:**
|
||||
- **Operating system and architecture:**
|
||||
- **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
|
||||
|
||||
---
|
||||
|
||||
*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.*
|
||||
|
||||
*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.*
|
||||
|
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<!-- 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 -->
|
||||
|
||||
---
|
||||
##### Contributor checklist
|
||||
<!-- For completed items, change [ ] to [x]. -->
|
||||
- [ ] Introduces security considerations
|
||||
- [ ] Affects the development, build or deployment processes of the component
|
17
.gitignore
vendored
17
.gitignore
vendored
@ -10,8 +10,9 @@ pids
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
# Coverage directory used by tools like istanbul/nyc
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
@ -24,14 +25,22 @@ build/Release
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
|
||||
node_modules
|
||||
|
||||
npm-shrinkwrap.json
|
||||
package-lock.json
|
||||
.resinconf
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
36
.resinci.yml
Normal file
36
.resinci.yml
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
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:
|
||||
publish: false
|
37
.travis.yml
37
.travis.yml
@ -1,22 +1,25 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "10"
|
||||
matrix:
|
||||
include:
|
||||
- node_js:
|
||||
- '6'
|
||||
env:
|
||||
- CAN_DEPLOY=true
|
||||
before_install:
|
||||
- npm -g install npm@4
|
||||
script: npm run ci
|
||||
exclude:
|
||||
node_js: "10"
|
||||
script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run ci
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
notifications:
|
||||
email: false
|
||||
deploy:
|
||||
provider: npm
|
||||
email: accounts@resin.io
|
||||
api_key:
|
||||
secure: phet6Du13hc1bzStbmpwy2ODNL5BFwjAmnpJ5wMcbWfI7fl0OtQ61s2+vW5hJAvm9fiRLOfiGAEiqOOtoupShZ1X8BNkC708d8+V+iZMoFh3+j6wAEz+N1sVq471PywlOuLAscOcqQNp92giCVt+4VPx2WQYh06nLsunvysGmUM=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$CAN_DEPLOY = 'true' && $TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: resin-io/resin-cli
|
||||
- provider: script
|
||||
script: npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: balena-io/balena-cli
|
||||
|
5497
.versionbot/CHANGELOG.yml
Normal file
5497
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
1209
CHANGELOG.md
1209
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
65
CONTRIBUTING.md
Normal file
65
CONTRIBUTING.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Contributing
|
||||
|
||||
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:
|
||||
|
||||
* `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.
|
||||
|
||||
Before opening a PR, please be sure to test your changes with `npm test`.
|
||||
|
||||
## Semantic versioning and commit messages
|
||||
|
||||
The CLI version numbering adheres to [Semantic Versioning](http://semver.org/). The following
|
||||
header/row is required in the body of a commit message, and will cause the CI build to fail if absent:
|
||||
|
||||
```
|
||||
Change-type: patch|minor|major
|
||||
```
|
||||
|
||||
Version numbers and commit messages are automatically added to the `CHANGELOG.md` file by the CI
|
||||
build flow, after a pull request is merged. It should not be manually edited.
|
||||
|
||||
## Editing documentation files (CHANGELOG, README, website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
|
||||
Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
|
||||
* Selected sections of the README file.
|
||||
* The CLI's command documentation in source code (both Capitano and oclif commands), for example:
|
||||
* `lib/actions/build.coffee`
|
||||
* `lib/actions-oclif/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`doc/cli.markdown` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
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.
|
||||
|
||||
## TypeScript vs CoffeeScript, and Capitano vs oclif
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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).
|
204
INSTALL.md
Normal file
204
INSTALL.md
Normal file
@ -0,0 +1,204 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
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).
|
||||
* [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.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
> **Windows users:**
|
||||
> * There is a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg) for installing
|
||||
> 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.
|
||||
|
||||
## Executable Installer
|
||||
|
||||
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`
|
||||
|
||||
2. Double click the downloaded file to run the installer.
|
||||
_If you are using macOS Catalina (10.15), [check this known issue and
|
||||
workaround](https://github.com/balena-io/balena-cli/issues/1479)._
|
||||
|
||||
3. After the installation completes, close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows so that the changes made by the installer to the PATH environment variable can take
|
||||
effect. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
|
||||
* `balena version` - should print the installed CLI version
|
||||
* `balena help` - should print the balena CLI help
|
||||
|
||||
> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
|
||||
> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
|
||||
> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
|
||||
> instructions for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
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`
|
||||
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)._
|
||||
|
||||
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
|
||||
as described above.
|
||||
|
||||
## NPM Installation
|
||||
|
||||
If you are a Node.js developer, you may wish to install the balena CLI via [npm](https://www.npmjs.com).
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
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
|
||||
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"`
|
||||
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
|
||||
* **Linux** and **Windows Subsystem for Linux (WSL):**
|
||||
`sudo apt-get install -y python git make g++`
|
||||
* **macOS:** install Apple's Command Line Tools by running on a Terminal window:
|
||||
`xcode-select --install`
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
|
||||
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `ssh`, `rsync`
|
||||
and more:
|
||||
* `pacman -S git openssh rsync gcc make`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
|
||||
interactive CLI menus to misbehave. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
|
||||
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
|
||||
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (which
|
||||
provides Python 2.7 and more), by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
|
||||
`npm install -g --production windows-build-tools`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is only required on systems where the global install directory is not user-writable.
|
||||
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
|
||||
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
|
||||
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
* The `balena ssh` command requires a recent version of the `ssh` command-line tool to be available:
|
||||
* macOS and Linux usually already have it installed. Otherwise, search for the available packages
|
||||
on your specific Linux distribution, or for the Mac consider the [Xcode command-line
|
||||
tools](https://developer.apple.com/xcode/features/) or [homebrew](https://brew.sh/).
|
||||
|
||||
* Microsoft started distributing an SSH client with Windows 10, which we understand is
|
||||
automatically installed through Windows Update, but can be manually installed too
|
||||
([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 `balena preload`, `balena build` and `balena deploy --build` commands require
|
||||
[Docker](https://docs.docker.com/install/overview/) or [balenaEngine](https://www.balena.io/engine/)
|
||||
to be available:
|
||||
* The `balena preload` command requires the Docker Engine to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Docker Desktop for Mac and
|
||||
Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1, so
|
||||
the workaround is to downgrade to version 18.06.1 (links: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
and [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)).
|
||||
See more details in [CLI issue 1099](https://github.com/balena-io/balena-cli/issues/1099).
|
||||
* Commonly, Docker is installed on the same machine where the CLI is being used, but the
|
||||
`balena build` and `balena deploy` commands can also use a remote Docker Engine (daemon)
|
||||
or balenaEngine (which could be a remote device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)) by specifying
|
||||
its IP address and port number as command-line options. Check the documentation for each
|
||||
command, e.g. `balena help build`, or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
* If you are using Microsoft's [Windows Subsystem for
|
||||
Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) and Docker Desktop for
|
||||
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).
|
||||
|
||||
## Configuring SSH keys
|
||||
|
||||
The `balena ssh` command requires an SSH key to be added to your balena account. If you had
|
||||
already added a SSH key in order to [deploy with 'git push'](https://www.balena.io/docs/learn/getting-started/raspberrypi3/nodejs/#adding-an-ssh-key),
|
||||
then you are probably done and may skip this section. You can check whether you already have
|
||||
an SSH key in your balena account with the `balena keys` command, or by visiting the
|
||||
[balena web dashboard](https://dashboard.balena-cloud.com/), clicking on your name -> Preferences
|
||||
-> SSH Keys.
|
||||
|
||||
> Note: An "SSH key" actually consists of a public/private key pair. A typical name for the private
|
||||
> key file is "id_rsa", and a typical name for the public key file is "id_rsa.pub". Both key files
|
||||
> are saved to your computer (with the private key optionally protected by a password), but only
|
||||
> the public key is saved to your balena account. This means that if you change computers or
|
||||
> otherwise lose the private key, _you cannot recover the private key through your balena account._
|
||||
> You can however add new keys, and delete the old ones.
|
||||
|
||||
If you don't have an SSH key in your balena account:
|
||||
|
||||
* If you have an existing SSH key in your computer that you would like to use, you can add it
|
||||
to your balena account through the balena web dashboard (Preferences -> SSH Keys), or through
|
||||
the CLI itself:
|
||||
|
||||
```bash
|
||||
# Windows 10 (cmd.exe prompt) example:
|
||||
$ balena key add MyKey %userprofile%\.ssh\id_rsa.pub
|
||||
# Linux / macOS example:
|
||||
$ balena key add MyKey ~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
* To generate a new key, you can follow [GitHub's documentation](https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent),
|
||||
skipping the step about adding the key to your GitHub account, and instead adding the key to
|
||||
your balena account as described above.
|
141
README.md
141
README.md
@ -1,98 +1,99 @@
|
||||
Resin CLI
|
||||
=========
|
||||
# balena CLI
|
||||
|
||||
> The official resin.io CLI tool.
|
||||
The official balena CLI tool.
|
||||
|
||||
[](http://badge.fury.io/js/resin-cli)
|
||||
[](https://david-dm.org/resin-io/resin-cli)
|
||||
[](https://gitter.im/resin-io/chat)
|
||||
[](http://badge.fury.io/js/balena-cli)
|
||||
[](https://david-dm.org/balena-io/balena-cli)
|
||||
|
||||
Requisites
|
||||
----------
|
||||
## About
|
||||
|
||||
- [NodeJS](https://nodejs.org) (>= v4)
|
||||
- [Git](https://git-scm.com)
|
||||
- The following executables should be correctly installed in your shell environment:
|
||||
- `ssh`: Any recent version of the OpenSSH ssh client (required by `resin sync` and `resin ssh`)
|
||||
- if you need `ssh` to work behind the proxy you also need [`proxytunnel`](http://proxytunnel.sourceforge.net/) installed (available as `proxytunnel` package for Ubuntu, for example)
|
||||
- `rsync`: >= 2.6.9 (required by `resin sync`)
|
||||
The balena CLI (Command-Line Interface) allows you to interact with the balenaCloud and the
|
||||
[balena API](https://www.balena.io/docs/reference/api/overview/) through a terminal window
|
||||
on Linux, macOS or Windows. You can also write shell scripts around it, or import its Node.js
|
||||
modules to use it programmatically.
|
||||
As an [open-source project on GitHub](https://github.com/balena-io/balena-cli/), your contribution
|
||||
is also welcome!
|
||||
|
||||
##### Windows Support
|
||||
## Installation
|
||||
|
||||
Before installing resin-cli, you'll need a working node-gyp environment. If you don't already have one you'll see native module build errors during installation. To fix this, run `npm install -g --production windows-build-tools` in an administrator console (available as 'Command Prompt (Admin)' when pressing windows+x in Windows 7+).
|
||||
Check the [balena CLI installation instructions on GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
`resin sync` and `resin ssh` have not been thoroughly tested on the standard Windows cmd.exe shell. We recommend using bash (or a similar) shell, like Bash for Windows 10 or [Git for Windows](https://git-for-windows.github.io/).
|
||||
## Getting Started
|
||||
|
||||
If you still want to use `cmd.exe` you will have to use a package manager like MinGW or chocolatey. For MinGW the steps are:
|
||||
### Choosing a shell (command prompt/terminal)
|
||||
|
||||
1. Install [MinGW](http://www.mingw.org).
|
||||
2. Install the `msys-rsync` and `msys-openssh` packages.
|
||||
3. Add MinGW to the `%PATH%` if this hasn't been done by the installer already. The location where the binaries are places is usually `C:\MinGW\msys\1.0\bin`, but it can vary if you selected a different location in the installer.
|
||||
4. Copy your SSH keys to `%homedrive%%homepath\.ssh`.
|
||||
5. If you need `ssh` to work behind the proxy you also need to install [proxytunnel](http://proxytunnel.sourceforge.net/)
|
||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||
[PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6)
|
||||
are supported. We are aware of users also having a good experience with alternative shells,
|
||||
including:
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git openssh rsync`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
|
||||
menus to break. [Check this Github issue for a
|
||||
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/)
|
||||
* 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
|
||||
[FAQ](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md) for using balena
|
||||
CLI with WSL and Docker Desktop for Windows.
|
||||
|
||||
### Install
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
This might require elevated privileges in some environments.
|
||||
### Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ npm install --global --production resin-cli
|
||||
$ balena login
|
||||
```
|
||||
|
||||
### Login
|
||||
### Proxy support
|
||||
|
||||
```sh
|
||||
$ resin login
|
||||
```
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
|
||||
_(Typically useful, but not strictly required for all commands)_
|
||||
* 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).
|
||||
|
||||
### Run commands
|
||||
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).
|
||||
|
||||
Take a look at the full command documentation at [https://docs.resin.io/tools/cli/](https://docs.resin.io/tools/cli/#table-of-contents
|
||||
), or by running `resin help`.
|
||||
## Command reference documentation
|
||||
|
||||
---
|
||||
The full CLI command reference is available [on the web](https://www.balena.io/docs/reference/cli/
|
||||
) or by running `balena help` and `balena help --verbose`.
|
||||
|
||||
Plugins
|
||||
-------
|
||||
## Support, FAQ and troubleshooting
|
||||
|
||||
The Resin CLI can be extended with plugins to automate laborious tasks and overall provide a better experience when working with Resin.io. Check the [plugin development tutorial](https://github.com/resin-io/resin-plugin-hello) to learn how to build your own!
|
||||
If you come across any problems or would like to get in touch:
|
||||
|
||||
FAQ
|
||||
---
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md).
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud).
|
||||
* 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/).
|
||||
|
||||
### Where is my configuration file?
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
The per-user configuration file lives in `$HOME/.resinrc.yml` or `%UserProfile%\_resinrc.yml`, in Unix based operating systems and Windows respectively.
|
||||
Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before
|
||||
submitting a pull request or updating documentation (because some files are automatically
|
||||
generated). Thank you for your help and interest!
|
||||
|
||||
The Resin CLI also attempts to read a `resinrc.yml` file in the current directory, which takes precedence over the per-user configuration file.
|
||||
## License
|
||||
|
||||
### How do I point the Resin CLI to staging?
|
||||
|
||||
The easiest way is to set the `RESINRC_RESIN_URL=resinstaging.io` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `resinUrl: resinstaging.io` to persist this setting.
|
||||
|
||||
### How do I make the Resin CLI persist data in another directory?
|
||||
|
||||
The Resin CLI persists your session token, as well as cached images in `$HOME/.resin` or `%UserProfile%\_resin`.
|
||||
|
||||
Pointing the Resin CLI to persist data in another location is necessary in certain environments, like a server, where there is no home directory, or a device running resinOS, which erases all data after a restart.
|
||||
|
||||
You can accomplish this by setting `RESINRC_DATA_DIRECTORY=/opt/resin` or adding `dataDirectory: /opt/resin` to your configuration file, replacing `/opt/resin` with your desired directory.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
If you're having any problems, check our [troubleshooting guide](https://github.com/resin-io/resin-cli/blob/master/TROUBLESHOOTING.md) and if your problem is not addressed there, please [raise an issue](https://github.com/resin-io/resin-cli/issues/new) on GitHub and the resin.io team will be happy to help.
|
||||
|
||||
You can also get in touch with us in the resin.io [forums](https://forums.resin.io/).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The project is licensed under the Apache 2.0 license.
|
||||
The project is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
A copy is also available in the LICENSE file in this repository.
|
||||
|
@ -1,15 +1,41 @@
|
||||
Troubleshooting
|
||||
===============
|
||||
# FAQ & Troubleshooting
|
||||
|
||||
This document contains common issues related to the Resin CLI, and how to fix them.
|
||||
This document contains some common issues, questions and answers related to the balena CLI.
|
||||
|
||||
### After burning to an sdcard, my device doesn't boot
|
||||
## Where is my configuration file?
|
||||
|
||||
The per-user configuration file lives in `$HOME/.balenarc.yml` or `%UserProfile%\_balenarc.yml`, in
|
||||
Unix based operating systems and Windows respectively.
|
||||
|
||||
The balena CLI also attempts to read a `balenarc.yml` file in the current directory, which takes
|
||||
precedence over the per-user configuration file.
|
||||
|
||||
## How do I point the balena CLI to staging?
|
||||
|
||||
The easiest way is to set the `BALENARC_BALENA_URL=balena-staging.com` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `balenaUrl: balena-staging.com` to
|
||||
persist this setting.
|
||||
|
||||
## How do I make the balena CLI persist data in another directory?
|
||||
|
||||
The balena CLI persists your session token, as well as cached images in `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`.
|
||||
|
||||
Pointing the balena CLI to persist data in another location is necessary in certain environments,
|
||||
like a server, where there is no home directory, or a device running balenaOS, which erases all
|
||||
data after a restart.
|
||||
|
||||
You can accomplish this by setting `BALENARC_DATA_DIRECTORY=/opt/balena` or adding `dataDirectory:
|
||||
/opt/balena` to your configuration file, replacing `/opt/balena` with your desired directory.
|
||||
|
||||
## After burning to an sdcard, my device doesn't boot
|
||||
|
||||
- The downloaded image is not complete (download was interrupted).
|
||||
|
||||
Please clean the cache (`%HOME/.resin/cache` or `C:\Users\<user>\_resin\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
Please clean the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
|
||||
### I get a permission error when burning to an sdcard
|
||||
## I get a permission error when burning to an sdcard
|
||||
|
||||
- The SDCard is locked.
|
||||
|
||||
@ -24,36 +50,79 @@ net.js:156
|
||||
Error: EINVAL, invalid argument
|
||||
at new Socket (net.js:156:18)
|
||||
at process.stdin (node.js:664:19)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
```
|
||||
|
||||
- Some interactive widgets don't work on `Cygwin`. If you're running Windows, it's preferrable that you use `cmd.exe`, as `Cygwin` is [not official supported by Node.js](https://github.com/chjj/blessed/issues/56#issuecomment-42671945).
|
||||
|
||||
### I get `Invalid MBR boot signature` when configuring a device
|
||||
## I get `Invalid MBR boot signature` when configuring a device
|
||||
|
||||
This error, accompanied with something like: `Expected 0xAA55, but saw 0x29FE` usually indicates a corrupted device operating system image in the cache, due to bad a internet connection during the download process.
|
||||
|
||||
Try clearing the cache with the following command and trying again:
|
||||
|
||||
```sh
|
||||
$ rm -rf $HOME/.resin/cache
|
||||
$ rm -rf $HOME/.balena/cache
|
||||
```
|
||||
|
||||
Or in Windows:
|
||||
|
||||
```sh
|
||||
> del /s /q %UserProfile%\_resin\cache
|
||||
> del /s /q %UserProfile%\_balena\cache
|
||||
```
|
||||
|
||||
### I get `EACCES: permission denied` when logging in
|
||||
## I get `EACCES: permission denied` when logging in
|
||||
|
||||
The Resin CLI stores the session token in `$HOME/.resin` or `C:\Users\<user>\_resin` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the Resin CLI as `root`, and thus the directory got owned by him.
|
||||
The balena CLI stores the session token in `$HOME/.balena` or `C:\Users\<user>\_balena` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the balena CLI as `root`, and thus the directory got owned by him.
|
||||
|
||||
Try resetting the ownership by running:
|
||||
|
||||
```sh
|
||||
$ sudo chown -R <user> $HOME/.resin
|
||||
$ sudo chown -R <user> $HOME/.balena
|
||||
```
|
||||
|
||||
## Broken line wrapping / cursor behavior with `balena ssh`
|
||||
|
||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example when long command lines are typed in a `balena ssh` session, or when using text editors like `vim` or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile` and the like), including UTF-8 misconfiguration, the use of unsupported ASCII control characters in shell prompt formatting (e.g. the `$PS1` env var) or the output of tools or log files that use colored text. The issue can sometimes be fixed by resizing the client terminal window, or by running one or more of the following commands on the shell:
|
||||
|
||||
```sh
|
||||
export TERMINAL=linux
|
||||
stty sane
|
||||
shopt -s checkwinsize
|
||||
bind 'set horizontal-scroll-mode off'
|
||||
```
|
||||
|
||||
Terminal multiplexer tools like GNU `screen` or `tmux` are sometimes reported to fix the issues, though at other times they are reported as the _cause_ of the problem. They have their own configuration files to take into account.
|
||||
|
||||
Further reference:
|
||||
* https://stackoverflow.com/questions/1133031/shell-prompt-line-wrapping-issue
|
||||
* https://superuser.com/questions/46948/any-way-to-fix-screens-mishandling-of-line-wrap-maybe-only-terminal-app
|
||||
* https://unix.stackexchange.com/questions/105958/terminal-prompt-not-wrapping-correctly
|
||||
* https://unix.stackexchange.com/questions/529377/terminal-long-line-wrapping
|
||||
* https://github.com/microsoft/WSL/issues/1436
|
||||
|
||||
If nothing seems to help, consider also using a different client-side terminal application:
|
||||
* Linux: xterm, KDE Konsole, GNOME Terminal
|
||||
* Mac: Terminal, iTerm2
|
||||
* Windows: PowerShell, PuTTY, WSL (Windows Subsystem for Linux)
|
||||
|
||||
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
|
||||
|
||||
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
|
||||
zip package for Linux. However, commands like "balena build" that contact a local Docker daemon,
|
||||
like the Docker Desktop for Windows, will try to reach Docker at the Unix socket path
|
||||
`/var/run/docker.sock`, while Docker Desktop for Windows uses a Windows named pipe at
|
||||
`//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A solution is:
|
||||
|
||||
- Open the Docker Desktop for Windows settings panel and tick the checkbox _"Expose daemon on tcp://localhost:2375 without TLS"._
|
||||
- On the WSL command line, set an env var:
|
||||
`export DOCKER_HOST=tcp://localhost:2375`
|
||||
Alternatively, use the command-line options `-h 127.0.0.1 -p 2375` for commands like `balena build` and `balena deploy`.
|
||||
|
||||
Further reference:
|
||||
|
||||
- https://techcommunity.microsoft.com/t5/Containers/WSL-Interoperability-with-Docker/ba-p/382405
|
||||
- https://forums.docker.com/t/wsl-and-docker-for-windows-cannot-connect-to-the-docker-daemon-at-tcp-localhost-2375-is-the-docker-daemon-running/63571/12
|
||||
|
43
appveyor.yml
Normal file
43
appveyor.yml
Normal file
@ -0,0 +1,43 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%AppData%\npm-cache'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
# what combinations to test
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm config set python 'C:\Python27\python.exe'
|
||||
- npm --version
|
||||
# - npm install
|
||||
|
||||
build: off
|
||||
test: off
|
||||
deploy: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm test
|
||||
|
||||
deploy_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
# - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
# - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
376
automation/build-bin.ts
Normal file
376
automation/build-bin.ts
Normal file
@ -0,0 +1,376 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile, spawn } from 'child_process';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as filehound from 'filehound';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as _ from 'lodash';
|
||||
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'));
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
|
||||
function dPath(...paths: string[]) {
|
||||
return path.join(ROOT, 'dist', ...paths);
|
||||
}
|
||||
|
||||
interface PathByPlatform {
|
||||
[platform: string]: string;
|
||||
}
|
||||
|
||||
const standaloneZips: PathByPlatform = {
|
||||
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||
};
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-installer.pkg`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
|
||||
};
|
||||
|
||||
export const finalReleaseAssets: { [platform: string]: string[] } = {
|
||||
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
|
||||
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
|
||||
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.
|
||||
* Also copy a number of native modules (binary '.node' files) that are
|
||||
* compiled during 'npm install' to the 'build-bin' folder, alongside
|
||||
* the single large executable file created by pkg. (This is necessary
|
||||
* because of a pkg limitation that does not allow binary executables
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
const args = [
|
||||
'--target',
|
||||
'host',
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
];
|
||||
console.log('=======================================================');
|
||||
console.log(`execPkg ${args.join(' ')}`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
|
||||
await execPkg(args);
|
||||
|
||||
const xpaths: Array<[string, string[]]> = [
|
||||
// [platform, [path, to, file]]
|
||||
['*', ['opn', 'xdg-open']],
|
||||
['darwin', ['denymount', 'bin', 'denymount']],
|
||||
];
|
||||
await Bluebird.map(xpaths, ([platform, xpath]) => {
|
||||
if (platform === '*' || platform === process.platform) {
|
||||
// eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
|
||||
return fs.copy(
|
||||
path.join(ROOT, 'node_modules', ...xpath),
|
||||
path.join(ROOT, 'build-bin', xpath.pop()!),
|
||||
);
|
||||
}
|
||||
});
|
||||
const nativeExtensionPaths: string[] = await filehound
|
||||
.create()
|
||||
.paths(path.join(ROOT, 'node_modules'))
|
||||
.ext(['node', 'dll'])
|
||||
.find();
|
||||
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
||||
|
||||
await Bluebird.map(nativeExtensionPaths, extPath =>
|
||||
fs.copy(
|
||||
extPath,
|
||||
extPath.replace(
|
||||
path.join(ROOT, 'node_modules'),
|
||||
path.join(ROOT, 'build-bin'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run some basic tests on the built pkg executable.
|
||||
* TODO: test more than just `balena version -j`; integrate with the
|
||||
* existing mocha/chai CLI command testing.
|
||||
*/
|
||||
async function testPkg() {
|
||||
type JsonVersions = import('../lib/actions-oclif/version').JsonVersions;
|
||||
const pkgBalenaPath = path.join(
|
||||
ROOT,
|
||||
'build-bin',
|
||||
process.platform === 'win32' ? 'balena.exe' : 'balena',
|
||||
);
|
||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
||||
// reported Node.js major version matches semver.major(process.version)
|
||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
const balenaVersions: JsonVersions = JSON.parse(stdout);
|
||||
pkgNodeVersion = balenaVersions['Node.js'];
|
||||
pkgNodeMajorVersion = semver.major(pkgNodeVersion);
|
||||
} catch (err) {
|
||||
throw new Error(stripIndent`
|
||||
Error parsing JSON output of "balena version -j": ${err}
|
||||
Original output: "${stdout}"`);
|
||||
}
|
||||
if (semver.major(process.version) !== pkgNodeMajorVersion) {
|
||||
throw new Error(
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${
|
||||
process.version
|
||||
}"`,
|
||||
);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the zip file for the standalone 'pkg' bundle previously created
|
||||
* by the buildPkg() function in 'build-bin.ts'.
|
||||
*/
|
||||
async function zipPkg() {
|
||||
const outputFile = standaloneZips[process.platform];
|
||||
if (!outputFile) {
|
||||
throw new Error(
|
||||
`Standalone installer unavailable for platform "${process.platform}"`,
|
||||
);
|
||||
}
|
||||
await fs.mkdirp(path.dirname(outputFile));
|
||||
await new Promise((resolve, reject) => {
|
||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 7 },
|
||||
});
|
||||
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
|
||||
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
outputStream.on('close', resolve);
|
||||
outputStream.on('error', reject);
|
||||
|
||||
archive.on('error', reject);
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
await buildPkg();
|
||||
await testPkg();
|
||||
await zipPkg();
|
||||
} catch (error) {
|
||||
console.log(`Error creating or testing standalone zip package:\n ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Standalone zip package build completed`);
|
||||
}
|
||||
|
||||
async function renameInstallerFiles() {
|
||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||
await fs.rename(
|
||||
oclifInstallers[process.platform],
|
||||
renamedOclifInstallers[process.platform],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||
*/
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
'-f',
|
||||
exeName,
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
*/
|
||||
export async function buildOclifInstaller() {
|
||||
let packOS = '';
|
||||
let packOpts = ['-r', ROOT];
|
||||
if (process.platform === 'darwin') {
|
||||
packOS = 'macos';
|
||||
} else if (process.platform === 'win32') {
|
||||
packOS = 'win';
|
||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||
}
|
||||
if (packOS) {
|
||||
console.log(`Building oclif installer for CLI ${version}`);
|
||||
const packCmd = `pack:${packOS}`;
|
||||
const dirs = [path.join(ROOT, 'dist', packOS)];
|
||||
if (packOS === 'win') {
|
||||
dirs.push(path.join(ROOT, 'tmp', 'win*'));
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback(cb => rimraf(dir, cb));
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
await renameInstallerFiles();
|
||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||
// The macOS installer is automatically signed by oclif (which runs the
|
||||
// `pkgbuild` tool), using the certificate name given in package.json
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
152
automation/capitanodoc/capitanodoc.ts
Normal file
152
automation/capitanodoc/capitanodoc.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { MarkdownFileParser } from './utils';
|
||||
|
||||
/**
|
||||
* This is the skeleton of CLI documentation/reference web page at:
|
||||
* https://www.balena.io/docs/reference/cli/
|
||||
*
|
||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||
* some content to this object.
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'Balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/actions/api-key.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: ['build/actions/app.js'],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: ['build/actions/auth.js'],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: ['build/actions/device.js'],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/actions-oclif/envs.js',
|
||||
'build/actions-oclif/env/add.js',
|
||||
'build/actions-oclif/env/rename.js',
|
||||
'build/actions-oclif/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: ['build/actions/tags.js'],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['build/actions/help.js', 'build/actions-oclif/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: ['build/actions/keys.js'],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/actions/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/actions/scan.js',
|
||||
'build/actions/ssh.js',
|
||||
'build/actions/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions/notes.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: ['build/actions/os.js', 'build/actions-oclif/os/configure.js'],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: ['build/actions/config.js'],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/actions/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/actions/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: ['build/actions/local/index.js'],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/actions/build.js', 'build/actions/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/actions/join.js', 'build/actions/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/actions/util.js'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify and return the `capitanoDoc` object above in order to render the
|
||||
* CLI documentation/reference web page at:
|
||||
* https://www.balena.io/docs/reference/cli/
|
||||
*
|
||||
* This function parses the README.md file to extract relevant sections
|
||||
* for the documentation web page.
|
||||
*/
|
||||
export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
const readmePath = path.join(__dirname, '..', '..', 'README.md');
|
||||
const mdParser = new MarkdownFileParser(readmePath);
|
||||
const sections: string[] = await Promise.all([
|
||||
mdParser.getSectionOfTitle('About').then((sectionLines: string) => {
|
||||
// delete the title of the 'About' section for the web page
|
||||
const match = /^(#+)\s+.+?\n\s*([^]*)/.exec(sectionLines);
|
||||
if (!match || match.length < 3) {
|
||||
throw new Error(`Error parsing section title`);
|
||||
}
|
||||
// match[1] has the title, match[2] has the rest
|
||||
return match && match[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Getting Started'),
|
||||
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
|
||||
]);
|
||||
capitanoDoc.introduction = sections.join('\n');
|
||||
return capitanoDoc;
|
||||
}
|
33
automation/capitanodoc/doc-types.d.ts
vendored
Normal file
33
automation/capitanodoc/doc-types.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import { CommandDefinition as CapitanoCommand } from 'capitano';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
export interface Document {
|
||||
title: string;
|
||||
introduction: string;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: Array<CapitanoCommand | OclifCommand>;
|
||||
}
|
||||
|
||||
export { CapitanoCommand, OclifCommand };
|
89
automation/capitanodoc/index.ts
Normal file
89
automation/capitanodoc/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getCapitanoDoc } from './capitanodoc';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
* page on the web: https://www.balena.io/docs/reference/cli/
|
||||
*/
|
||||
export async function renderMarkdown(): Promise<string> {
|
||||
const capitanodoc = await getCapitanoDoc();
|
||||
const result: Document = {
|
||||
title: capitanodoc.title,
|
||||
introduction: capitanodoc.introduction,
|
||||
categories: [],
|
||||
};
|
||||
|
||||
for (const commandCategory of capitanodoc.categories) {
|
||||
const category: Category = {
|
||||
title: commandCategory.title,
|
||||
commands: [],
|
||||
};
|
||||
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(
|
||||
...(jsFilename.includes('actions-oclif')
|
||||
? importOclifCommands(jsFilename)
|
||||
: importCapitanoCommands(jsFilename)),
|
||||
);
|
||||
}
|
||||
result.categories.push(category);
|
||||
}
|
||||
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
|
||||
const actions = require(path.join(process.cwd(), jsFilename));
|
||||
const commands: CapitanoCommand[] = [];
|
||||
|
||||
if (actions.signature) {
|
||||
commands.push(_.omit(actions, 'action'));
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action'));
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
return [command];
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the CLI docs markdown to stdout.
|
||||
* See package.json for how the output is redirected to a file.
|
||||
*/
|
||||
async function printMarkdown() {
|
||||
try {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
printMarkdown();
|
160
automation/capitanodoc/markdown.ts
Normal file
160
automation/capitanodoc/markdown.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||
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];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result.push('### Options');
|
||||
|
||||
for (const option of command.options!) {
|
||||
if (option == null) {
|
||||
throw new Error(`Undefined option in markdown generation!`);
|
||||
}
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage)}`];
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
.join('\n')
|
||||
.trim();
|
||||
result.push(description);
|
||||
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.flags)) {
|
||||
result.push('### Options');
|
||||
for (const [name, flag] of Object.entries(command.flags!)) {
|
||||
if (name === 'help') {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
result.push(`#### ${flagUsage}`);
|
||||
result.push(flag.description || '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result.push(
|
||||
...(typeof command === 'object'
|
||||
? renderCapitanoCommand(command)
|
||||
: renderOclifCommand(command)),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAnchor(cmdSignature: string): string {
|
||||
return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function renderToc(categories: Category[]): string[] {
|
||||
const result = [`# CLI Command Reference`];
|
||||
|
||||
for (const category of categories) {
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map(command => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
: capitanoizeOclifUsage(command.usage); // oclif
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
OS: [
|
||||
'os versions',
|
||||
'os download',
|
||||
'os build config',
|
||||
'os configure',
|
||||
'os initialize',
|
||||
],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<CapitanoCommand | OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
...renderToc(doc.categories),
|
||||
];
|
||||
for (const category of doc.categories) {
|
||||
result.push(...renderCategory(category));
|
||||
}
|
||||
return result.join('\n\n');
|
||||
}
|
139
automation/capitanodoc/utils.ts
Normal file
139
automation/capitanodoc/utils.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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) {
|
||||
if (signature.length > 1) {
|
||||
return '--';
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionSignature(signature: string) {
|
||||
return `${getOptionPrefix(signature)}${signature}`;
|
||||
}
|
||||
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (_.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (_.isString(option.alias)) {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
if (option.parameter) {
|
||||
result += ` <${option.parameter}>`;
|
||||
}
|
||||
|
||||
return ent.encode(result);
|
||||
}
|
||||
|
||||
export class MarkdownFileParser {
|
||||
constructor(public mdFilePath: string) {}
|
||||
|
||||
/**
|
||||
* Extract the lines of a markdown document section with the given title.
|
||||
* For example, consider this sample markdown document:
|
||||
* ```
|
||||
* # balena CLI
|
||||
*
|
||||
* ## Introduction
|
||||
* Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
*
|
||||
* ## Getting Started
|
||||
* sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
*
|
||||
* ### Prerequisites
|
||||
* - Foo
|
||||
* - Bar
|
||||
*
|
||||
* ## Support
|
||||
* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
|
||||
* ```
|
||||
*
|
||||
* Calling getSectionOfTitle('Getting Started') for the markdown doc above
|
||||
* returns everything from line '## Getting Started' (included) to line
|
||||
* '## Support' (excluded). This method counts the number of '#' characters
|
||||
* to determine that subsections should be included as part of the parent
|
||||
* section.
|
||||
*
|
||||
* @param title The section title without '#' chars, eg. 'Getting Started'
|
||||
*/
|
||||
public async getSectionOfTitle(
|
||||
title: string,
|
||||
includeSubsections = true,
|
||||
): Promise<string> {
|
||||
let foundSectionLines: string[];
|
||||
let foundSectionLevel = 0;
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(this.mdFilePath),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
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"
|
||||
const match = /^(#+)\s+(.+)/.exec(line);
|
||||
if (match) {
|
||||
const sectionLevel = match[1].length;
|
||||
const sectionTitle = match[2];
|
||||
|
||||
// If the target section had already been found: append a line, or end it
|
||||
if (foundSectionLines) {
|
||||
if (!includeSubsections || sectionLevel <= foundSectionLevel) {
|
||||
// end previously found section
|
||||
rl.close();
|
||||
}
|
||||
} else if (sectionTitle === title) {
|
||||
// found the target section
|
||||
foundSectionLevel = sectionLevel;
|
||||
foundSectionLines = [];
|
||||
}
|
||||
}
|
||||
if (foundSectionLines) {
|
||||
foundSectionLines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
rl.on('close', () => {
|
||||
if (foundSectionLines) {
|
||||
resolve(foundSectionLines.join('\n'));
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${
|
||||
this.mdFilePath
|
||||
}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
75
automation/check-npm-version.js
Normal file
75
automation/check-npm-version.js
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Check that semver v1 is greater than or equal to semver v2.
|
||||
*
|
||||
* 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}'`);
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
} else if (v1Array[i] > v2Array[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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 requiredVersion = '6.9.0';
|
||||
if (!semverGte(npmVersion, requiredVersion)) {
|
||||
// In case you take issue with the error message below:
|
||||
// "At this point, however, your 'npm-shrinkwrap.json' file has
|
||||
// already been damaged"
|
||||
// ... and think: "why not add the check to the 'preinstall' hook?",
|
||||
// the reason is that it would unnecessarily prevent end users from
|
||||
// using npm v6.4.1 that ships with Node 8. (It is OK for the
|
||||
// shrinkwrap file to get damaged if it is not going to be reused.)
|
||||
console.error(`\
|
||||
-------------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
258
automation/deploy-bin.ts
Normal file
258
automation/deploy-bin.ts
Normal file
@ -0,0 +1,258 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
|
||||
/**
|
||||
* Create or update a release in GitHub's releases page, uploading the
|
||||
* installer files (standalone zip + native oclif installers).
|
||||
*/
|
||||
export async function createGitHubRelease() {
|
||||
console.log(`Publishing release ${version} to GitHub`);
|
||||
const publishRelease = await import('publish-release');
|
||||
const ghRelease = await Bluebird.fromCallback(
|
||||
publishRelease.bind(null, {
|
||||
token: GITHUB_TOKEN || '',
|
||||
owner: 'balena-io',
|
||||
repo: 'balena-cli',
|
||||
tag: version,
|
||||
name: `balena-CLI ${version}`,
|
||||
reuseRelease: true,
|
||||
assets: finalReleaseAssets[process.platform],
|
||||
}),
|
||||
);
|
||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level function to create a CLI release in GitHub's releases page:
|
||||
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
||||
* display on the releases page, and call createGitHubRelease() to upload
|
||||
* the files.
|
||||
*/
|
||||
export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
require('@octokit/plugin-throttling'),
|
||||
);
|
||||
return (cachedOctokit = new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${
|
||||
options.url
|
||||
}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (_retryAfter: number, options: any) => {
|
||||
// does not retry, only logs a warning
|
||||
console.warn(
|
||||
`Abuse detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract pagination information (current page, total pages, ordinal number)
|
||||
* from the 'link' response header (example below), using the parse-link-header
|
||||
* npm package:
|
||||
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
||||
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
||||
*
|
||||
* @param response Octokit response object (including response.headers.link)
|
||||
* @param perPageDefault Default per_page pagination value if missing in URL
|
||||
* @return Object where 'page' is the current page number (1-based),
|
||||
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
||||
* (3rd, 4th, 5th...) of the first item in the current page.
|
||||
*/
|
||||
function getPageNumbers(
|
||||
response: any,
|
||||
perPageDefault: number,
|
||||
): { page: number; pages: number; ordinal: number } {
|
||||
const res = { page: 1, pages: 1, ordinal: 1 };
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = require('parse-link-header');
|
||||
const parsed = parse(response.headers.link);
|
||||
let perPage = perPageDefault;
|
||||
if (parsed.next) {
|
||||
if (parsed.next.per_page) {
|
||||
perPage = parseInt(parsed.next.per_page, 10);
|
||||
}
|
||||
res.page = parseInt(parsed.next.page, 10) - 1;
|
||||
res.pages = parseInt(parsed.last.page, 10);
|
||||
} else {
|
||||
if (parsed.prev.per_page) {
|
||||
perPage = parseInt(parsed.prev.per_page, 10);
|
||||
}
|
||||
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
||||
}
|
||||
res.ordinal = (res.page - 1) * perPage + 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over every GitHub release in the given owner/repo, check whether
|
||||
* its tag_name matches against the affectedVersions semver spec, and if so
|
||||
* replace its release description (body) with the given newDescription value.
|
||||
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
||||
* @param repo GitHub repo, e.g. 'balena-cli'
|
||||
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
||||
* @param newDescription New release description (body)
|
||||
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
||||
* can be searched to determine whether that release has already been updated.
|
||||
*/
|
||||
async function updateGitHubReleaseDescriptions(
|
||||
owner: string,
|
||||
repo: string,
|
||||
affectedVersions: string,
|
||||
newDescription: string,
|
||||
editID: string,
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
for await (const response of octokit.paginate.iterator(options)) {
|
||||
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
|
||||
response,
|
||||
perPage,
|
||||
);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
if (!cliRelease.id) {
|
||||
console.error(
|
||||
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
||||
console.info(`${skipMsg}: already updated`);
|
||||
continue;
|
||||
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
||||
console.info(`${skipMsg}: outside version range`);
|
||||
continue;
|
||||
} else {
|
||||
const updatedRelease = {
|
||||
owner,
|
||||
repo,
|
||||
release_id: cliRelease.id,
|
||||
body: newDescription,
|
||||
};
|
||||
let oldBodyPreview = cliRelease.body;
|
||||
if (oldBodyPreview) {
|
||||
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
||||
if (oldBodyPreview.length > 12) {
|
||||
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${
|
||||
cliRelease.id
|
||||
}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning description to CLI releases affected by a mixpanel tracking
|
||||
* security issue (#1359). This function can be executed "manually" with the
|
||||
* following command line:
|
||||
*
|
||||
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
||||
*/
|
||||
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
||||
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
||||
// (It could have been any other platform, as long as it only runs once.)
|
||||
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
||||
return;
|
||||
}
|
||||
const owner = 'balena-io';
|
||||
const repo = 'balena-cli';
|
||||
const affectedVersions =
|
||||
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
||||
const editID = '[AA100]';
|
||||
let newDescription = `
|
||||
Please note: the "login" command in this release is affected by a
|
||||
security issue fixed in versions
|
||||
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
||||
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
||||
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
||||
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
||||
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
||||
and later. If you need to use this version, avoid passing your password,
|
||||
keys or tokens as command-line arguments. ${editID}`;
|
||||
// remove line breaks and collapse white space
|
||||
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
||||
await updateGitHubReleaseDescriptions(
|
||||
owner,
|
||||
repo,
|
||||
affectedVersions,
|
||||
newDescription,
|
||||
editID,
|
||||
);
|
||||
}
|
115
automation/run.ts
Normal file
115
automation/run.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
fixPathForMsys,
|
||||
ROOT,
|
||||
runUnderMsys,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
throw error; // to please the Typescript compiler
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
* 'build:installer' (to build a native oclif installer)
|
||||
* 'build:standalone' (to build a standalone pkg package)
|
||||
* 'release' (to create/update a GitHub release)
|
||||
*
|
||||
* In the case of 'build:installer', also call runUnderMsys() to switch the
|
||||
* shell from cmd.exe to MSYS2 bash.exe.
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If runUnderMsys() is called to re-execute this script under MSYS2,
|
||||
// the current working dir becomes the MSYS2 homedir, so we change back.
|
||||
process.chdir(ROOT);
|
||||
|
||||
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||
// (patched) to copy/extract the CLI files, run npm install and then
|
||||
// create the NSIS executable installer for Windows. This was necessary
|
||||
// to avoid issues with a 260-char limit on Windows paths (possibly a
|
||||
// limitation of some library used by NSIS), as the "current working dir"
|
||||
// provided by balena CI is a rather long path to start with.
|
||||
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
|
||||
const randID = require('crypto')
|
||||
.randomBytes(6)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_'); // base64url (RFC 4648)
|
||||
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
try {
|
||||
if (arg === 'build:installer' && process.platform === 'win32') {
|
||||
// ensure running under MSYS2
|
||||
if (!process.env.MSYSTEM) {
|
||||
process.env.MSYS2_PATH_TYPE = 'inherit';
|
||||
await runUnderMsys([
|
||||
fixPathForMsys(process.argv[0]),
|
||||
fixPathForMsys(process.argv[1]),
|
||||
arg,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
|
||||
throw new Error(
|
||||
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
|
||||
);
|
||||
}
|
||||
}
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
return exitWithError(`"${arg}": ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
19
automation/tsconfig.json
Normal file
19
automation/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
73
balena-completion.bash
Normal file
73
balena-completion.bash
Normal file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
scan ssh util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
12
bin/balena
Executable file
12
bin/balena
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheFile: __dirname + '/.fast-boot.json'
|
||||
})
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
29
bin/balena-dev
Executable file
29
bin/balena-dev
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PERROSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
require('coffeescript/register');
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||
// it is supposed to run faster. We still benefit from type checking when
|
||||
// running 'npm run build'.
|
||||
require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../lib/app').run();
|
@ -1,115 +0,0 @@
|
||||
# coffeelint: disable=max_line_length
|
||||
|
||||
module.exports =
|
||||
title: 'Resin CLI Documentation'
|
||||
introduction: '''
|
||||
This tool allows you to interact with the resin.io api from the comfort of your command line.
|
||||
|
||||
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
|
||||
|
||||
To get started download the CLI from npm.
|
||||
|
||||
$ npm install resin-cli -g
|
||||
|
||||
Then authenticate yourself:
|
||||
|
||||
$ resin login
|
||||
|
||||
Now you have access to all the commands referenced below.
|
||||
|
||||
## Proxy support
|
||||
|
||||
The CLI does support HTTP(S) proxies.
|
||||
|
||||
You can configure the proxy using several methods (in order of their precedence):
|
||||
|
||||
* set the `RESINRC_PROXY` environment variable in the URL format (with protocol, host, port, and optionally the basic auth),
|
||||
* use the [resin config file](https://www.npmjs.com/package/resin-settings-client#documentation) (project-specific or user-level)
|
||||
and set the `proxy` setting. This can be:
|
||||
* a string in the URL format,
|
||||
* or an object following [this format](https://www.npmjs.com/package/global-tunnel-ng#options), which allows more control,
|
||||
* or set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
'''
|
||||
|
||||
categories: [
|
||||
{
|
||||
title: 'Application'
|
||||
files: [ 'lib/actions/app.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [ 'lib/actions/auth.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [ 'lib/actions/device.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [ 'lib/actions/environment-variables.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
files: [ 'lib/actions/help.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Information',
|
||||
files: [ 'lib/actions/info.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [ 'lib/actions/keys.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: [ 'lib/actions/logs.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Sync',
|
||||
files: [ 'lib/actions/sync.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
files: [ 'lib/actions/ssh.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: [ 'lib/actions/notes.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [ 'lib/actions/os.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [ 'lib/actions/config.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: [ 'lib/actions/preload.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: [ 'lib/actions/settings.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Wizard',
|
||||
files: [ 'lib/actions/wizard.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [ 'lib/actions/local/index.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: [
|
||||
'lib/actions/build.coffee'
|
||||
'lib/actions/deploy.coffee'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: [ 'lib/actions/util.coffee' ]
|
||||
},
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Provisioning Resin.io devices in automated (non-interactive) mode
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
@ -7,7 +7,7 @@ It requires collecting some preliminary information _once_.
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
resin device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
@ -20,15 +20,15 @@ But before you can run it you need to collect the parameters and build the confi
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
resin devices supported
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
1. `APP_ID`. Create an application (`resin app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`resin apps`) and notice its ID.
|
||||
1. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
resin os versions DEVICE_TYPE
|
||||
balena os versions DEVICE_TYPE
|
||||
```
|
||||
and pick the version that you need, like _v2.0.6+rev1.prod_.
|
||||
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
|
||||
@ -36,10 +36,10 @@ But before you can run it you need to collect the parameters and build the confi
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
resin util available-drives
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
The resin CLI will not display the system drives to protect you,
|
||||
The balena CLI will not display the system drives to protect you,
|
||||
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
|
||||
|
||||
Now we have all the parameters -- time to build the config file.
|
||||
@ -50,21 +50,21 @@ Interactive device provisioning process often includes collecting some extra dev
|
||||
|
||||
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
|
||||
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./resin-os/raspberrypi3-config.json_.
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
|
||||
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./resin-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
|
||||
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
resin os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
resin os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
```
|
||||
|
||||
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
|
||||
@ -97,11 +97,11 @@ There are several ways to eliminate it and make the process fully non-interactiv
|
||||
|
||||
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
|
||||
|
||||
But if you're using a machine dedicated to resin provisioning this can be fine, and also the simplest thing to do.
|
||||
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
You can configure the `resin` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
|
||||
### Extra initialization config
|
||||
|
||||
@ -109,4 +109,4 @@ As of June 2017 all the supported devices should not require any other interacti
|
||||
|
||||
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
|
||||
|
||||
If that is the case please raise the issue in the resin CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
||||
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
||||
|
1680
doc/cli.markdown
1680
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
@ -1,46 +0,0 @@
|
||||
_ = require('lodash')
|
||||
path = require('path')
|
||||
capitanodoc = require('../../capitanodoc')
|
||||
markdown = require('./markdown')
|
||||
|
||||
result = {}
|
||||
result.title = capitanodoc.title
|
||||
result.introduction = capitanodoc.introduction
|
||||
result.categories = []
|
||||
|
||||
for commandCategory in capitanodoc.categories
|
||||
category = {}
|
||||
category.title = commandCategory.title
|
||||
category.commands = []
|
||||
|
||||
for file in commandCategory.files
|
||||
actions = require(path.join(process.cwd(), file))
|
||||
|
||||
if actions.signature?
|
||||
category.commands.push(_.omit(actions, 'action'))
|
||||
else
|
||||
for actionName, actionCommand of actions
|
||||
category.commands.push(_.omit(actionCommand, 'action'))
|
||||
|
||||
result.categories.push(category)
|
||||
|
||||
result.toc = _.cloneDeep(result.categories)
|
||||
result.toc = _.map result.toc, (category) ->
|
||||
category.commands = _.map category.commands, (command) ->
|
||||
return {
|
||||
signature: command.signature
|
||||
anchor: '#' + command.signature
|
||||
.replace(/\s/g,'-')
|
||||
.replace(/</g, '60-')
|
||||
.replace(/>/g, '-62-')
|
||||
.replace(/\[/g, '')
|
||||
.replace(/\]/g, '-')
|
||||
.replace(/--/g, '-')
|
||||
.replace(/\.\.\./g, '')
|
||||
.replace(/\|/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
return category
|
||||
|
||||
console.log(markdown.display(result))
|
@ -1,66 +0,0 @@
|
||||
_ = require('lodash')
|
||||
ent = require('ent')
|
||||
utils = require('./utils')
|
||||
|
||||
exports.command = (command) ->
|
||||
result = """
|
||||
## #{ent.encode(command.signature)}
|
||||
|
||||
#{command.help}\n
|
||||
"""
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
result += '\n### Options'
|
||||
|
||||
for option in command.options
|
||||
result += """
|
||||
\n\n#### #{utils.parseSignature(option)}
|
||||
|
||||
#{option.description}
|
||||
"""
|
||||
|
||||
result += '\n'
|
||||
|
||||
return result
|
||||
|
||||
exports.category = (category) ->
|
||||
result = """
|
||||
# #{category.title}\n
|
||||
"""
|
||||
|
||||
for command in category.commands
|
||||
result += '\n' + exports.command(command)
|
||||
|
||||
return result
|
||||
|
||||
exports.toc = (toc) ->
|
||||
result = '''
|
||||
# Table of contents\n
|
||||
'''
|
||||
|
||||
for category in toc
|
||||
|
||||
result += """
|
||||
\n- #{category.title}\n\n
|
||||
"""
|
||||
|
||||
for command in category.commands
|
||||
result += """
|
||||
\t- [#{ent.encode(command.signature)}](#{command.anchor})\n
|
||||
"""
|
||||
|
||||
return result
|
||||
|
||||
exports.display = (doc) ->
|
||||
result = """
|
||||
# #{doc.title}
|
||||
|
||||
#{doc.introduction}
|
||||
|
||||
#{exports.toc(doc.toc)}
|
||||
"""
|
||||
|
||||
for category in doc.categories
|
||||
result += '\n' + exports.category(category)
|
||||
|
||||
return result
|
@ -1,26 +0,0 @@
|
||||
_ = require('lodash')
|
||||
ent = require('ent')
|
||||
|
||||
exports.getOptionPrefix = (signature) ->
|
||||
if signature.length > 1
|
||||
return '--'
|
||||
else
|
||||
return '-'
|
||||
|
||||
exports.getOptionSignature = (signature) ->
|
||||
return "#{exports.getOptionPrefix(signature)}#{signature}"
|
||||
|
||||
exports.parseSignature = (option) ->
|
||||
result = exports.getOptionSignature(option.signature)
|
||||
|
||||
if not _.isEmpty(option.alias)
|
||||
if _.isString(option.alias)
|
||||
result += ", #{exports.getOptionSignature(option.alias)}"
|
||||
else
|
||||
for alias in option.alias
|
||||
result += ", #{exports.getOptionSignature(option.alias)}"
|
||||
|
||||
if option.parameter?
|
||||
result += " <#{option.parameter}>"
|
||||
|
||||
return ent.encode(result)
|
@ -1,19 +1,15 @@
|
||||
path = require('path')
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
coffeelint = require('gulp-coffeelint')
|
||||
inlinesource = require('gulp-inline-source')
|
||||
mocha = require('gulp-mocha')
|
||||
shell = require('gulp-shell')
|
||||
packageJSON = require('./package.json')
|
||||
|
||||
OPTIONS =
|
||||
config:
|
||||
coffeelint: path.join(__dirname, 'coffeelint.json')
|
||||
files:
|
||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||
app: 'lib/**/*.coffee'
|
||||
tests: 'tests/**/*.spec.coffee'
|
||||
tests: 'tests/**/*.spec.js'
|
||||
pages: 'lib/auth/pages/*.ejs'
|
||||
directories:
|
||||
build: 'build/'
|
||||
@ -23,28 +19,15 @@ gulp.task 'pages', ->
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages'))
|
||||
|
||||
gulp.task 'coffee', [ 'lint' ], ->
|
||||
gulp.task 'coffee', ->
|
||||
gulp.src(OPTIONS.files.app)
|
||||
.pipe(coffee(bare: true, header: true))
|
||||
.pipe(gulp.dest(OPTIONS.directories.build))
|
||||
|
||||
gulp.task 'lint', ->
|
||||
gulp.src(OPTIONS.files.coffee)
|
||||
.pipe(coffeelint({
|
||||
optFile: OPTIONS.config.coffeelint
|
||||
}))
|
||||
.pipe(coffeelint.reporter())
|
||||
|
||||
gulp.task 'test', ->
|
||||
gulp.src(OPTIONS.files.tests, read: false)
|
||||
.pipe(mocha({
|
||||
reporter: 'min'
|
||||
}))
|
||||
|
||||
gulp.task 'build', [
|
||||
gulp.task 'build', gulp.series [
|
||||
'coffee',
|
||||
'pages'
|
||||
]
|
||||
|
||||
gulp.task 'watch', [ 'build' ], ->
|
||||
gulp.task 'watch', gulp.series [ 'build' ], ->
|
||||
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])
|
||||
|
138
lib/actions-oclif/env/add.ts
vendored
Normal file
138
lib/actions-oclif/env/add.ts
vendored
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
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 or device, as selected
|
||||
by the respective command-line options.
|
||||
|
||||
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.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'environment or config variable name',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
public static usage =
|
||||
'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),
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvAddCmd,
|
||||
);
|
||||
const cmd = this;
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { exitWithExpectedError } = await import('../../utils/patterns');
|
||||
|
||||
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}`,
|
||||
);
|
||||
} else if (!options.quiet) {
|
||||
cmd.warn(
|
||||
`Using ${params.name}=${params.value} from CLI process environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
_.startsWith(params.name, prefix),
|
||||
);
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
|
||||
if (options.application) {
|
||||
await balena.models.application[varType].set(
|
||||
options.application,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} else if (options.device) {
|
||||
await balena.models.device[varType].set(
|
||||
options.device,
|
||||
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();
|
||||
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
});
|
||||
|
||||
return response.body.reservedNamespaces;
|
||||
}
|
96
lib/actions-oclif/env/rename.ts
vendored
Normal file
96
lib/actions-oclif/env/rename.ts
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @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 { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
device: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
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 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.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples modify variables that apply to all services in an app or device.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env rename 376 emacs',
|
||||
'$ balena env rename 376 emacs --device',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: 'environment variable numeric database ID',
|
||||
parse: input => parseInt(input, 10),
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: true,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' 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',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = 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',
|
||||
id: params.id,
|
||||
body: {
|
||||
value: params.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
120
lib/actions-oclif/env/rm.ts
vendored
Normal file
120
lib/actions-oclif/env/rm.ts
vendored
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
config: boolean;
|
||||
device: boolean;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class EnvRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an environment variable from an application or device.
|
||||
|
||||
Remove a configuration or environment variable from an application or device,
|
||||
as selected by command-line options.
|
||||
|
||||
Note that this command asks for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
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.
|
||||
`;
|
||||
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',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: 'environment variable numeric database ID',
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' 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,
|
||||
}),
|
||||
yes: flags.boolean({
|
||||
char: 'y',
|
||||
description: 'Run in non-interactive mode',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRmCmd,
|
||||
);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
if (isNaN(params.id) || !Number.isInteger(Number(params.id))) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The environment variable id must be an integer',
|
||||
);
|
||||
}
|
||||
|
||||
await patterns.confirm(
|
||||
options.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',
|
||||
id: params.id,
|
||||
});
|
||||
}
|
||||
}
|
95
lib/actions-oclif/envs.ts
Normal file
95
lib/actions-oclif/envs.ts
Normal file
@ -0,0 +1,95 @@
|
||||
/**
|
||||
* @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 { Command, flags } from '@oclif/command';
|
||||
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { CommandHelp } from '../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
config: boolean;
|
||||
device?: string;
|
||||
help: void;
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
export default class EnvsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the environment or config variables of an app or device.
|
||||
|
||||
List the environment or config variables of an application or device,
|
||||
as selected by the respective command-line options.
|
||||
|
||||
The --config option is used to list "configuration variables" that
|
||||
control balena features.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples list variables that apply to all services in an app or device.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'envs ' + new CommandHelp({ args: EnvsCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: _.assign({ exclusive: ['device'] }, cf.application),
|
||||
config: flags.boolean({
|
||||
char: 'c',
|
||||
description: 'show config variables',
|
||||
}),
|
||||
device: _.assign({ exclusive: ['application'] }, cf.device),
|
||||
help: cf.help,
|
||||
verbose: cf.verbose,
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
} else {
|
||||
return exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
|
||||
if (_.isEmpty(environmentVariables)) {
|
||||
return exitWithExpectedError('No environment variables found');
|
||||
}
|
||||
|
||||
cmd.log(
|
||||
visuals.table.horizontal(environmentVariables, ['id', 'name', 'value']),
|
||||
);
|
||||
}
|
||||
}
|
431
lib/actions-oclif/os/configure.ts
Normal file
431
lib/actions-oclif/os/configure.ts
Normal file
@ -0,0 +1,431 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import BalenaSdk = require('balena-sdk');
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
advanced?: boolean;
|
||||
app?: string;
|
||||
application?: string;
|
||||
config?: string;
|
||||
'config-app-update-poll-interval'?: number;
|
||||
'config-network'?: string;
|
||||
'config-wifi-key'?: string;
|
||||
'config-wifi-ssid'?: string;
|
||||
device?: string; // device UUID
|
||||
'device-api-key'?: string;
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface DeferredDevice extends BalenaSdk.Device {
|
||||
belongs_to__application: BalenaSdk.PineDeferred;
|
||||
}
|
||||
|
||||
interface Answers {
|
||||
appUpdatePollInterval: number; // in minutes
|
||||
deviceType: string; // e.g. "raspberrypi3"
|
||||
network: 'ethernet' | 'wifi';
|
||||
version: string; // e.g. "2.32.0+rev1"
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
}
|
||||
|
||||
const deviceApiKeyDeprecationMsg = stripIndent`
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.`;
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Configure a previously downloaded balenaOS image.
|
||||
|
||||
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\`
|
||||
2. A given \`config.json\` file specified with the \`--config\` option.
|
||||
3. User input through interactive prompts (text menus).
|
||||
|
||||
The --device-type option may be used to override the application's default
|
||||
device type, in case of an application with mixed device types.
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
required: true,
|
||||
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'os configure' to avoid oclif's 'os:configure' topic syntax
|
||||
public static usage =
|
||||
'os configure ' +
|
||||
new CommandHelp({ args: OsConfigureCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
advanced: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'ask advanced configuration questions (when in interactive mode)',
|
||||
}),
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device'],
|
||||
}),
|
||||
application: _.assign({ exclusive: ['app', 'device'] }, cf.application),
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
'interval (in minutes) for the on-device balena supervisor periodic app update check',
|
||||
}),
|
||||
'config-network': flags.string({
|
||||
description: 'device network type (non-interactive configuration)',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
'config-wifi-key': flags.string({
|
||||
description: 'WiFi key (password) (non-interactive configuration)',
|
||||
}),
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: _.assign({ exclusive: ['app', 'application'] }, cf.device),
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
|
||||
}),
|
||||
'device-type': flags.string({
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the application device type',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsConfigureCmd,
|
||||
);
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
options.app = undefined;
|
||||
|
||||
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');
|
||||
let app: BalenaSdk.Application | undefined;
|
||||
let device: BalenaSdk.Device | undefined;
|
||||
let deviceTypeSlug: string;
|
||||
|
||||
if (options.device) {
|
||||
device = await balena.models['device'].get(options.device);
|
||||
deviceTypeSlug = device.device_type;
|
||||
} else {
|
||||
app = await balena.models['application'].get(options.application!);
|
||||
await checkDeviceTypeCompatibility(balena, options, app);
|
||||
deviceTypeSlug = options['device-type'] || app.device_type;
|
||||
}
|
||||
|
||||
const deviceTypeManifest = await helpers.getManifest(
|
||||
params.image,
|
||||
deviceTypeSlug,
|
||||
);
|
||||
|
||||
let configJson: import('../../utils/config').ImgConfig | undefined;
|
||||
if (options.config) {
|
||||
const rawConfig = await fs.readFile(options.config, 'utf8');
|
||||
configJson = JSON.parse(rawConfig);
|
||||
}
|
||||
|
||||
const answers: Answers = await askQuestionsForDeviceType(
|
||||
deviceTypeManifest,
|
||||
options,
|
||||
configJson,
|
||||
);
|
||||
if (options.application) {
|
||||
answers.deviceType = deviceTypeSlug;
|
||||
}
|
||||
answers.version =
|
||||
options.version ||
|
||||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
configJson = await generateDeviceConfig(
|
||||
device as DeferredDevice,
|
||||
options['device-api-key'],
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
configJson = await generateApplicationConfig(app!, answers);
|
||||
}
|
||||
}
|
||||
|
||||
console.info('Configuring operating system image');
|
||||
|
||||
await helpers.osProgressHandler(
|
||||
await devInit.configure(
|
||||
params.image,
|
||||
deviceTypeManifest,
|
||||
configJson || {},
|
||||
answers,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOptions(options: FlagsDef) {
|
||||
if (process.platform === 'win32') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Unsupported platform error: the 'balena os configure' command currently requires
|
||||
the Windows Subsystem for Linux in order to run on Windows. It was tested with
|
||||
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
|
||||
release for Linux (rather than Windows) should be installed: for example, the
|
||||
standalone zip package for Linux. (It is possible to have both a Windows CLI
|
||||
release and a Linux CLI release installed simultaneously.) For more information
|
||||
on WSL and the balena CLI installation options, please check:
|
||||
- https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
`);
|
||||
}
|
||||
// The 'device' and 'application' options are declared "exclusive" in the oclif
|
||||
// flag definitions above, so oclif will enforce that they are not both used together.
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
"Either the '--device' or the '--application' option must be provided",
|
||||
);
|
||||
}
|
||||
if (!options.application && options['device-type']) {
|
||||
throw new ExpectedError(
|
||||
"The '--device-type' option can only be used in conjunction with the '--application' option",
|
||||
);
|
||||
}
|
||||
if (options['device-api-key']) {
|
||||
console.error(stripIndent`
|
||||
-------------------------------------------------------------------------------------------
|
||||
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
|
||||
-------------------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
const { checkLoggedIn } = await import('../../utils/patterns');
|
||||
await checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around balena-device-init.getImageOsVersion(). Throws ExpectedError
|
||||
* if the OS image could not be read or the OS version could not be extracted
|
||||
* from it.
|
||||
* @param imagePath Local filesystem path to a balenaOS image file
|
||||
* @param deviceTypeManifest Device type manifest object
|
||||
*/
|
||||
async function getOsVersionFromImage(
|
||||
imagePath: string,
|
||||
deviceTypeManifest: BalenaSdk.DeviceType,
|
||||
devInit: typeof import('balena-device-init'),
|
||||
): Promise<string> {
|
||||
const osVersion = await devInit.getImageOsVersion(
|
||||
imagePath,
|
||||
deviceTypeManifest,
|
||||
);
|
||||
if (!osVersion) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not read OS version from the image. Please specify the balenaOS
|
||||
version manually with the --version command-line option.`);
|
||||
}
|
||||
return osVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that options['device-type'], e.g. 'raspberrypi3', is compatible with
|
||||
* app.device_type, e.g. 'raspberry-pi2'. Throws ExpectedError if they are not
|
||||
* compatible.
|
||||
* @param sdk Balena Node SDK instance
|
||||
* @param options oclif command-line options object
|
||||
* @param app Balena SDK Application model object
|
||||
*/
|
||||
async function checkDeviceTypeCompatibility(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
options: FlagsDef,
|
||||
app: BalenaSdk.Application,
|
||||
) {
|
||||
if (options['device-type']) {
|
||||
const [appDeviceType, optionDeviceType] = await Promise.all([
|
||||
sdk.models.device.getManifestBySlug(app.device_type),
|
||||
sdk.models.device.getManifestBySlug(options['device-type']),
|
||||
]);
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given options or configJson objects (in this order) contain
|
||||
* the answers to some configuration questions, and interactively ask the
|
||||
* user the questions for which answers are missing. Questions such as:
|
||||
*
|
||||
* ? Network Connection (Use arrow keys)
|
||||
* ethernet
|
||||
* ❯ wifi
|
||||
* ? Network Connection wifi
|
||||
* ? Wifi SSID i-ssid
|
||||
* ? Wifi Passphrase [input is hidden]
|
||||
*
|
||||
* The questions are extracted from the given deviceType "manifest".
|
||||
*/
|
||||
async function askQuestionsForDeviceType(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
options: FlagsDef,
|
||||
configJson?: import('../../utils/config').ImgConfig,
|
||||
): Promise<Answers> {
|
||||
const form = await import('resin-cli-form');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const answerSources: any[] = [camelifyConfigOptions(options)];
|
||||
const defaultAnswers: Partial<Answers> = {};
|
||||
const questions: any = deviceType.options;
|
||||
let extraOpts: { override: object } | undefined;
|
||||
|
||||
if (!_.isEmpty(configJson)) {
|
||||
answerSources.push(configJson);
|
||||
}
|
||||
|
||||
if (!options.advanced) {
|
||||
const advancedGroup: any = _.find(questions, {
|
||||
name: 'advanced',
|
||||
isGroup: true,
|
||||
});
|
||||
if (!_.isEmpty(advancedGroup)) {
|
||||
answerSources.push(helpers.getGroupDefaults(advancedGroup));
|
||||
}
|
||||
}
|
||||
|
||||
for (const questionName of getQuestionNames(deviceType)) {
|
||||
for (const answerSource of answerSources) {
|
||||
if (answerSource[questionName] != null) {
|
||||
defaultAnswers[questionName] = answerSource[questionName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!defaultAnswers.network &&
|
||||
(defaultAnswers.wifiSsid || defaultAnswers.wifiKey)
|
||||
) {
|
||||
defaultAnswers.network = 'wifi';
|
||||
}
|
||||
|
||||
if (!_.isEmpty(defaultAnswers)) {
|
||||
extraOpts = { override: defaultAnswers };
|
||||
}
|
||||
|
||||
return form.run(questions, extraOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a deviceType "manifest" containing "options" properties, return an
|
||||
* array of "question names" as in the following example.
|
||||
*
|
||||
* @param deviceType Device type "manifest", for example:
|
||||
* { "slug": "raspberrypi3",
|
||||
* "options": [{
|
||||
* "options": [ {
|
||||
* "name": "network",
|
||||
* "choices": ["ethernet", "wifi"],
|
||||
* ... }, {
|
||||
* "name": "wifiSsid",
|
||||
* "type": "text",
|
||||
* ... }, {
|
||||
* "options": [ {
|
||||
* "name": "appUpdatePollInterval",
|
||||
* "default": 10,
|
||||
* ...
|
||||
* @return Array of question names, for example:
|
||||
* [ 'network', 'wifiSsid', 'wifiKey', 'appUpdatePollInterval' ]
|
||||
*/
|
||||
function getQuestionNames(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
): Array<keyof Answers> {
|
||||
const questionNames: string[] = _.chain(deviceType.options)
|
||||
.flatMap(
|
||||
(group: BalenaSdk.DeviceTypeOptions) =>
|
||||
(group.isGroup && group.options) || [],
|
||||
)
|
||||
.map((groupOption: BalenaSdk.DeviceTypeOptionsGroup) => groupOption.name)
|
||||
.filter()
|
||||
.value();
|
||||
return questionNames as Array<keyof Answers>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new object with the key-value pairs from the input object,
|
||||
* renaming keys that start with the 'config-' prefix as follows:
|
||||
* Sample input:
|
||||
* { app: 'foo', 'config-wifi-key': 'mykey', 'config-wifi-ssid': 'myssid' }
|
||||
* Output:
|
||||
* { app: 'foo', wifiKey: 'mykey', wifiSsid: 'myssid' }
|
||||
*/
|
||||
function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
|
||||
return _.mapKeys(options, (_value, key) => {
|
||||
if (key.startsWith('config-')) {
|
||||
return key
|
||||
.substring('config-'.length)
|
||||
.replace(/-[a-z]/g, match => match.substring(1).toUpperCase());
|
||||
}
|
||||
return key;
|
||||
});
|
||||
}
|
85
lib/actions-oclif/version.ts
Normal file
85
lib/actions-oclif/version.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface FlagsDef {
|
||||
all?: boolean;
|
||||
json?: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export interface JsonVersions {
|
||||
'balena-cli': string;
|
||||
'Node.js': string;
|
||||
}
|
||||
|
||||
export default class VersionCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
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.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena version',
|
||||
'$ balena version -a',
|
||||
'$ balena version -j',
|
||||
];
|
||||
|
||||
public static usage = 'version';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
all: flags.boolean({
|
||||
char: 'a',
|
||||
default: false,
|
||||
description:
|
||||
'include version information for additional components (Node.js)',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
default: false,
|
||||
description:
|
||||
'output version information in JSON format for programmatic use',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(VersionCmd);
|
||||
const versions: JsonVersions = {
|
||||
'balena-cli': (await import('../../package.json')).version,
|
||||
'Node.js':
|
||||
process.version && process.version.startsWith('v')
|
||||
? process.version.slice(1)
|
||||
: process.version,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(versions, null, 4));
|
||||
} else {
|
||||
if (options.all) {
|
||||
console.log(`balena-cli version "${versions['balena-cli']}"`);
|
||||
console.log(`Node.js version "${versions['Node.js']}"`);
|
||||
} else {
|
||||
// backwards compatibility
|
||||
console.log(versions['balena-cli']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
lib/actions/api-key.ts
Normal file
36
lib/actions/api-key.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const generate: CommandDefinition<{
|
||||
name: string;
|
||||
}> = {
|
||||
signature: 'api-key generate <name>',
|
||||
description: 'Generate a new API key with the given name',
|
||||
help: stripIndent`
|
||||
This command generates a new API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using '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,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -20,44 +20,48 @@ exports.create =
|
||||
signature: 'app create <name>'
|
||||
description: 'create an application'
|
||||
help: '''
|
||||
Use this command to create a new resin.io application.
|
||||
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
|
||||
|
||||
$ resin devices supported
|
||||
$ balena devices supported
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app create MyApp
|
||||
$ resin app create MyApp --type raspberry-pi
|
||||
$ balena app create MyApp
|
||||
$ balena app create MyApp --type raspberry-pi
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
parameter: 'type'
|
||||
description: 'application device type (Check available types with `resin devices supported`)'
|
||||
description: 'application device type (Check available types with `balena devices supported`)'
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
# Validate the the application name is available
|
||||
# before asking the device type.
|
||||
# https://github.com/resin-io/resin-cli/issues/30
|
||||
resin.models.application.has(params.name).then (hasApplication) ->
|
||||
# https://github.com/balena-io/balena-cli/issues/30
|
||||
balena.models.application.has(params.name).then (hasApplication) ->
|
||||
if hasApplication
|
||||
throw new Error('You already have an application with that name!')
|
||||
patterns.exitWithExpectedError('You already have an application with that name!')
|
||||
|
||||
.then ->
|
||||
return options.type or patterns.selectDeviceType()
|
||||
.then (deviceType) ->
|
||||
return resin.models.application.create(params.name, 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)
|
||||
@ -69,25 +73,37 @@ exports.list =
|
||||
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 resin app <name> instead.
|
||||
If you want detailed information, use balena app <name> instead.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin apps
|
||||
$ balena apps
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.getAll().then (applications) ->
|
||||
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'
|
||||
'devices_length'
|
||||
'device_count'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
@ -99,15 +115,15 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app MyApp
|
||||
$ balena app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.get(params.name).then (application) ->
|
||||
balena.models.application.get(params.name).then (application) ->
|
||||
console.log visuals.table.vertical application, [
|
||||
"$#{application.app_name}$"
|
||||
'id'
|
||||
@ -125,33 +141,33 @@ exports.restart =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app restart MyApp
|
||||
$ balena app restart MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.application.restart(params.name).nodeify(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 resin.io application.
|
||||
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:
|
||||
|
||||
$ resin app rm MyApp
|
||||
$ resin app rm MyApp --yes
|
||||
$ balena app rm MyApp
|
||||
$ balena app rm MyApp --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then ->
|
||||
resin.models.application.remove(params.name)
|
||||
balena.models.application.remove(params.name)
|
||||
.nodeify(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -16,9 +16,9 @@ limitations under the License.
|
||||
|
||||
exports.login =
|
||||
signature: 'login'
|
||||
description: 'login to resin.io'
|
||||
description: 'login to balena'
|
||||
help: '''
|
||||
Use this command to login to your resin.io account.
|
||||
Use this command to login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
@ -27,20 +27,20 @@ exports.login =
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using the authentication token from the preferences page.
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin login
|
||||
$ resin login --web
|
||||
$ resin login --token "..."
|
||||
$ resin login --credentials
|
||||
$ resin login --credentials --email johndoe@gmail.com --password secret
|
||||
$ balena login
|
||||
$ balena login --web
|
||||
$ balena login --token "..."
|
||||
$ balena login --credentials
|
||||
$ balena login --credentials --email johndoe@gmail.com --password secret
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'auth token'
|
||||
description: 'session token or API key'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
@ -57,23 +57,23 @@ exports.login =
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
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')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
@ -84,10 +84,15 @@ exports.login =
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Token (from the preferences page)'
|
||||
message: 'Session token or API key from the preferences page'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(resin.auth.loginWithToken)
|
||||
.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
|
||||
@ -97,24 +102,25 @@ exports.login =
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
return capitanoRunAsync('signup')
|
||||
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)
|
||||
|
||||
resin.settings.get('resinUrl').then (resinUrl) ->
|
||||
console.log(messages.resinAsciiArt)
|
||||
console.log("\nLogging in to #{resinUrl}")
|
||||
balena.settings.get('balenaUrl').then (balenaUrl) ->
|
||||
console.log(messages.balenaAsciiArt)
|
||||
console.log("\nLogging in to #{balenaUrl}")
|
||||
return login(options)
|
||||
.then(resin.auth.whoami)
|
||||
.then(balena.auth.whoami)
|
||||
.tap (username) ->
|
||||
console.info("Successfully logged in as: #{username}")
|
||||
console.info """
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ resin help
|
||||
$ balena help
|
||||
|
||||
#{messages.reachingOut}
|
||||
"""
|
||||
@ -122,59 +128,17 @@ exports.login =
|
||||
|
||||
exports.logout =
|
||||
signature: 'logout'
|
||||
description: 'logout from resin.io'
|
||||
description: 'logout from balena'
|
||||
help: '''
|
||||
Use this command to logout from your resin.io account.o
|
||||
Use this command to logout from your balena account.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logout
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.auth.logout().nodeify(done)
|
||||
|
||||
exports.signup =
|
||||
signature: 'signup'
|
||||
description: 'signup to resin.io'
|
||||
help: '''
|
||||
Use this command to signup for a resin.io account.
|
||||
|
||||
If signup is successful, you'll be logged in to your new user automatically.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin signup
|
||||
Email: johndoe@acme.com
|
||||
Password: ***********
|
||||
|
||||
$ resin whoami
|
||||
johndoe
|
||||
$ balena logout
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
form = require('resin-cli-form')
|
||||
validation = require('../utils/validation')
|
||||
|
||||
resin.settings.get('resinUrl').then (resinUrl) ->
|
||||
console.log("\nRegistering to #{resinUrl}")
|
||||
|
||||
form.run [
|
||||
message: 'Email:'
|
||||
name: 'email'
|
||||
type: 'input'
|
||||
validate: validation.validateEmail
|
||||
,
|
||||
message: 'Password:'
|
||||
name: 'password'
|
||||
type: 'password',
|
||||
validate: validation.validatePassword
|
||||
]
|
||||
|
||||
.then(resin.auth.register)
|
||||
.then(resin.auth.loginWithToken)
|
||||
.nodeify(done)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.auth.logout().nodeify(done)
|
||||
|
||||
exports.whoami =
|
||||
signature: 'whoami'
|
||||
@ -184,18 +148,18 @@ exports.whoami =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin whoami
|
||||
$ balena whoami
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
email: resin.auth.getEmail()
|
||||
url: resin.settings.get('resinUrl')
|
||||
username: balena.auth.whoami()
|
||||
email: balena.auth.getEmail()
|
||||
url: balena.settings.get('balenaUrl')
|
||||
.then (results) ->
|
||||
console.log visuals.table.vertical results, [
|
||||
'$account information$'
|
||||
|
@ -2,42 +2,84 @@
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
getBundleInfo = Promise.method (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
if options.application?
|
||||
# An application was provided
|
||||
return helpers.getAppInfo(options.application)
|
||||
.then (app) ->
|
||||
return [app.arch, app.device_type]
|
||||
else if options.arch? and options.deviceType?
|
||||
return [options.arch, options.deviceType]
|
||||
else
|
||||
# No information, cannot do resolution
|
||||
return undefined
|
||||
app: the app this build is for (optional)
|
||||
arch: the architecture to build for
|
||||
deviceType: the device type to build for
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
buildProject = (docker, logger, composeOpts, opts) ->
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
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 container locally'
|
||||
permission: 'user'
|
||||
help: '''
|
||||
Use this command to build a container with a provided docker daemon.
|
||||
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 resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
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:
|
||||
|
||||
$ resin build
|
||||
$ resin build ./source/
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf
|
||||
$ resin build --application MyApp ./source/
|
||||
$ resin build --docker '/var/run/docker.sock'
|
||||
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
'''
|
||||
options: dockerUtils.appendOptions [
|
||||
$ 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'
|
||||
@ -53,12 +95,61 @@ module.exports =
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target resin.io application this build is for'
|
||||
description: 'The target balena application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Logger = require('../utils/logger')
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, new Logger())
|
||||
.asCallback(done)
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
|
||||
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)
|
||||
|
@ -1,100 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
exports.yes =
|
||||
signature: 'yes'
|
||||
description: 'confirm non interactively'
|
||||
boolean: true
|
||||
alias: 'y'
|
||||
|
||||
exports.optionalApplication =
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'application name'
|
||||
alias: [ 'a', 'app' ]
|
||||
|
||||
exports.application = _.defaults
|
||||
required: 'You have to specify an application'
|
||||
, exports.optionalApplication
|
||||
|
||||
exports.optionalDevice =
|
||||
signature: 'device'
|
||||
parameter: 'device'
|
||||
description: 'device uuid'
|
||||
alias: 'd'
|
||||
|
||||
exports.optionalDeviceApiKey =
|
||||
signature: 'deviceApiKey'
|
||||
description: 'custom device key - note that this is only supported on ResinOS 2.0.3+'
|
||||
parameter: 'device-api-key'
|
||||
alias: 'k'
|
||||
|
||||
exports.booleanDevice =
|
||||
signature: 'device'
|
||||
description: 'device'
|
||||
boolean: true
|
||||
alias: 'd'
|
||||
|
||||
exports.osVersion =
|
||||
signature: 'version'
|
||||
description: """
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)
|
||||
"""
|
||||
parameter: 'version'
|
||||
|
||||
exports.network =
|
||||
signature: 'network'
|
||||
parameter: 'network'
|
||||
description: 'network type'
|
||||
alias: 'n'
|
||||
|
||||
exports.wifiSsid =
|
||||
signature: 'ssid'
|
||||
parameter: 'ssid'
|
||||
description: 'wifi ssid, if network is wifi'
|
||||
alias: 's'
|
||||
|
||||
exports.wifiKey =
|
||||
signature: 'key'
|
||||
parameter: 'key'
|
||||
description: 'wifi key, if network is wifi'
|
||||
alias: 'k'
|
||||
|
||||
exports.forceUpdateLock =
|
||||
signature: 'force'
|
||||
description: 'force action if the update lock is set'
|
||||
boolean: true
|
||||
alias: 'f'
|
||||
|
||||
exports.drive =
|
||||
signature: 'drive'
|
||||
description: 'the drive to write the image to, like `/dev/sdb` or `/dev/mmcblk0`.
|
||||
Careful with this as you can erase your hard drive.
|
||||
Check `resin util available-drives` for available options.'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
|
||||
exports.advancedConfig =
|
||||
signature: 'advanced'
|
||||
description: 'show advanced configuration options'
|
||||
boolean: true
|
||||
alias: 'v'
|
157
lib/actions/command-options.ts
Normal file
157
lib/actions/command-options.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
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 _ = require('lodash');
|
||||
|
||||
export const yes = {
|
||||
signature: 'yes',
|
||||
description: 'confirm non interactively',
|
||||
boolean: true,
|
||||
alias: 'y',
|
||||
};
|
||||
|
||||
export interface YesOption {
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
export const optionalApplication = {
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'application name',
|
||||
alias: ['a', 'app'],
|
||||
};
|
||||
|
||||
export const application = _.defaults(
|
||||
{ required: 'You have to specify an application' },
|
||||
optionalApplication,
|
||||
);
|
||||
|
||||
export const optionalRelease = {
|
||||
signature: 'release',
|
||||
parameter: 'release',
|
||||
description: 'release id',
|
||||
alias: 'r',
|
||||
};
|
||||
|
||||
export const optionalDevice = {
|
||||
signature: 'device',
|
||||
parameter: 'device',
|
||||
description: 'device uuid',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const optionalDeviceApiKey = {
|
||||
signature: 'deviceApiKey',
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
parameter: 'device-api-key',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const optionalDeviceType = {
|
||||
signature: 'deviceType',
|
||||
description: 'device type slug',
|
||||
parameter: 'device-type',
|
||||
};
|
||||
|
||||
export const optionalOsVersion = {
|
||||
signature: 'version',
|
||||
description: 'a balenaOS version',
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export type OptionalOsVersionOption = Partial<OsVersionOption>;
|
||||
|
||||
export const osVersion = _.defaults(
|
||||
{
|
||||
required: 'You have to specify an exact os version',
|
||||
},
|
||||
exports.optionalOsVersion,
|
||||
);
|
||||
|
||||
export interface OsVersionOption {
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const booleanDevice = {
|
||||
signature: 'device',
|
||||
description: 'device',
|
||||
boolean: true,
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const osVersionOrSemver = {
|
||||
signature: 'version',
|
||||
description: `\
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)\
|
||||
`,
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export const network = {
|
||||
signature: 'network',
|
||||
parameter: 'network',
|
||||
description: 'network type',
|
||||
alias: 'n',
|
||||
};
|
||||
|
||||
export const wifiSsid = {
|
||||
signature: 'ssid',
|
||||
parameter: 'ssid',
|
||||
description: 'wifi ssid, if network is wifi',
|
||||
alias: 's',
|
||||
};
|
||||
|
||||
export const wifiKey = {
|
||||
signature: 'key',
|
||||
parameter: 'key',
|
||||
description: 'wifi key, if network is wifi',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const forceUpdateLock = {
|
||||
signature: 'force',
|
||||
description: 'force action if the update lock is set',
|
||||
boolean: true,
|
||||
alias: 'f',
|
||||
};
|
||||
|
||||
export const drive = {
|
||||
signature: 'drive',
|
||||
description: `the drive to write the image to, like \`/dev/sdb\` or \`/dev/mmcblk0\`. \
|
||||
Careful with this as you can erase your hard drive. \
|
||||
Check \`balena util available-drives\` for available options.`,
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const advancedConfig = {
|
||||
signature: 'advanced',
|
||||
description: 'show advanced configuration options',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
};
|
||||
|
||||
export const hostOSAccess = {
|
||||
signature: 'host',
|
||||
boolean: true,
|
||||
description: 'access host OS (for devices with balenaOS >= 2.0.0+rev1)',
|
||||
alias: 's',
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -15,6 +15,7 @@ limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.read =
|
||||
signature: 'config read'
|
||||
@ -24,13 +25,13 @@ exports.read =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config read --type raspberry-pi
|
||||
$ resin config read --type raspberry-pi --drive /dev/disk2
|
||||
$ 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 `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -46,7 +47,7 @@ exports.read =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
prettyjson = require('prettyjson')
|
||||
@ -68,14 +69,14 @@ exports.write =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config write --type raspberry-pi username johndoe
|
||||
$ resin config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ resin config write --type raspberry-pi files.network/settings "..."
|
||||
$ 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 `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -92,7 +93,7 @@ exports.write =
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
@ -116,17 +117,18 @@ 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) of a provisioned device"
|
||||
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:
|
||||
|
||||
$ resin config inject my/config.json --type raspberry-pi
|
||||
$ resin config inject my/config.json --type raspberry-pi --drive /dev/disk2
|
||||
$ 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 `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -142,7 +144,7 @@ exports.inject =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
@ -165,14 +167,14 @@ exports.reconfigure =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config reconfigure --type raspberry-pi
|
||||
$ resin config reconfigure --type raspberry-pi --advanced
|
||||
$ resin config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
$ 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 `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -194,9 +196,9 @@ exports.reconfigure =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
@ -207,10 +209,10 @@ exports.reconfigure =
|
||||
.tap ->
|
||||
umountAsync(drive)
|
||||
.then (uuid) ->
|
||||
configureCommand = "os configure #{drive} #{uuid}"
|
||||
configureCommand = "os configure #{drive} --device #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return capitanoRunAsync(configureCommand)
|
||||
return runCommand(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
@ -221,23 +223,38 @@ exports.generate =
|
||||
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:
|
||||
|
||||
$ resin config generate --device 7cf02a6
|
||||
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --output config.json
|
||||
$ resin config generate --app MyApp
|
||||
$ resin config generate --app MyApp --output config.json
|
||||
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
$ 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'
|
||||
@ -268,42 +285,71 @@ exports.generate =
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
deviceConfig = require('resin-device-config')
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
helpers = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
throw new Error '''
|
||||
exitWithExpectedError '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ resin help config generate
|
||||
$ 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 resin.models.device.get(options.device)
|
||||
return resin.models.application.get(options.application)
|
||||
return balena.models.device.get(options.device)
|
||||
return balena.models.application.get(options.application)
|
||||
.then (resource) ->
|
||||
resin.models.device.getManifestBySlug(resource.device_type)
|
||||
.get('options')
|
||||
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, answers)
|
||||
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
|
||||
else
|
||||
answers.deviceType = deviceType
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
deviceConfig.validate(config)
|
||||
if options.output?
|
||||
return writeFileAsync(options.output, JSON.stringify(config))
|
||||
|
||||
|
@ -1,145 +1,176 @@
|
||||
# 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')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app })
|
||||
"https://builder.#{baseUrl}/v1/push?#{args}"
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app, buildId })
|
||||
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
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()
|
||||
|
||||
formatImageName = (image) ->
|
||||
image.split('/').pop()
|
||||
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!')
|
||||
|
||||
parseInput = Promise.method (params, options) ->
|
||||
if not params.appName?
|
||||
throw new Error('Need an application to deploy to!')
|
||||
appName = params.appName
|
||||
image = undefined
|
||||
if params.image?
|
||||
if options.build or options.source?
|
||||
throw new Error('Build and source parameters are not applicable when specifying an image')
|
||||
options.build = false
|
||||
image = params.image
|
||||
else if options.build
|
||||
source = options.source || '.'
|
||||
else
|
||||
throw new Error('Need either an image or a build flag!')
|
||||
# find which services use images that already exist locally
|
||||
Promise.map project.descriptors, (d) ->
|
||||
# unconditionally build (or pull) if explicitly requested
|
||||
return d if opts.shouldPerformBuild
|
||||
docker.getImage(d.image.tag ? d.image).inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn()
|
||||
.filter (d) -> !!d
|
||||
.then (servicesToSkip) ->
|
||||
# multibuild takes in a composition and always attempts to
|
||||
# build or pull all services. we workaround that here by
|
||||
# passing a modified composition.
|
||||
compositionToBuild = _.cloneDeep(project.composition)
|
||||
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
|
||||
if _.size(compositionToBuild.services) is 0
|
||||
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
|
||||
return {}
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
compositionToBuild
|
||||
opts.app.arch
|
||||
opts.app.device_type
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then (builtImages) ->
|
||||
_.keyBy(builtImages, 'serviceName')
|
||||
.then (builtImages) ->
|
||||
project.descriptors.map (d) ->
|
||||
builtImages[d.serviceName] ? {
|
||||
serviceName: d.serviceName,
|
||||
name: d.image.tag ? d.image
|
||||
logs: 'Build skipped; image for service already exists.'
|
||||
props: {}
|
||||
}
|
||||
.then (images) ->
|
||||
if opts.app.application_type?[0]?.is_legacy
|
||||
chalk = require('chalk')
|
||||
legacyDeploy = require('../utils/deploy-legacy')
|
||||
|
||||
return [appName, options.build, source, image]
|
||||
msg = chalk.yellow('Target application requires legacy deploy method.')
|
||||
logger.logWarn(msg)
|
||||
|
||||
showPushProgress = (message) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
progressBar = new visuals.Progress(message)
|
||||
progressBar.update({ percentage: 0 })
|
||||
return progressBar
|
||||
|
||||
getBundleInfo = (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getAppInfo(options.appName)
|
||||
.then (app) ->
|
||||
[app.arch, app.device_type]
|
||||
|
||||
performUpload = (imageStream, token, username, url, appName, logger) ->
|
||||
request = require('request')
|
||||
progressStream = require('progress-stream')
|
||||
zlib = require('zlib')
|
||||
|
||||
# Need to strip off the newline
|
||||
progressMessage = logger.formatMessage('info', 'Deploying').slice(0, -1)
|
||||
progressBar = showPushProgress(progressMessage)
|
||||
streamWithProgress = imageStream.pipe progressStream
|
||||
time: 500,
|
||||
length: imageStream.length
|
||||
, ({ percentage, eta }) ->
|
||||
progressBar.update
|
||||
percentage: Math.min(percentage, 100)
|
||||
eta: eta
|
||||
|
||||
uploadRequest = request.post
|
||||
url: getBuilderPushEndpoint(url, username, appName)
|
||||
headers:
|
||||
'Content-Encoding': 'gzip'
|
||||
auth:
|
||||
bearer: token
|
||||
body: streamWithProgress.pipe(zlib.createGzip({
|
||||
level: 6
|
||||
}))
|
||||
|
||||
uploadToPromise(uploadRequest, logger)
|
||||
|
||||
uploadLogs = (logs, token, url, buildId, username, appName) ->
|
||||
request = require('request')
|
||||
request.post
|
||||
json: true
|
||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
|
||||
auth:
|
||||
bearer: token
|
||||
body: Buffer.from(logs)
|
||||
|
||||
uploadToPromise = (uploadRequest, logger) ->
|
||||
new Promise (resolve, reject) ->
|
||||
|
||||
handleMessage = (data) ->
|
||||
data = data.toString()
|
||||
logger.logDebug("Received data: #{data}")
|
||||
|
||||
try
|
||||
obj = JSON.parse(data)
|
||||
catch e
|
||||
logger.logError('Error parsing reply from remote side')
|
||||
reject(e)
|
||||
return
|
||||
|
||||
if obj.type?
|
||||
switch obj.type
|
||||
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
||||
when 'success' then resolve(obj)
|
||||
when 'status' then logger.logInfo("Remote: #{obj.message}")
|
||||
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
else
|
||||
reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
|
||||
uploadRequest
|
||||
.on('error', reject)
|
||||
.on('data', handleMessage)
|
||||
return Promise.join(
|
||||
docker
|
||||
logger
|
||||
sdk.auth.getToken()
|
||||
sdk.auth.whoami()
|
||||
sdk.settings.get('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 an image to a resin.io application'
|
||||
help: '''
|
||||
Use this command to deploy an image to an application, optionally building it first.
|
||||
|
||||
description: 'Deploy a single image or a multicontainer project to a balena application'
|
||||
help: """
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`resin deploy <appOwnerUsername>/<appName>`.
|
||||
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.)
|
||||
|
||||
Note: If building with this command, all options supported by `resin build`
|
||||
are also supported with this command.
|
||||
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:
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
'''
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage
|
||||
"""
|
||||
permission: 'user'
|
||||
options: dockerUtils.appendOptions [
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Build image then deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
primary: true
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'The source directory to use when building the image'
|
||||
description: 'Specify an alternate source directory; default is the working directory'
|
||||
alias: 's'
|
||||
},
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Force a rebuild before deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'nologupload'
|
||||
description: "Don't upload build logs to the dashboard with image (if building)"
|
||||
@ -147,83 +178,57 @@ module.exports =
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
sdk = (require('balena-sdk')).fromSharedOptions()
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
logger = new Logger()
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
logger = Logger.getLogger()
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
logs = ''
|
||||
# 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
|
||||
|
||||
upload = (token, username, url) ->
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
# Check input parameters
|
||||
parseInput(params, options)
|
||||
.then ([appName, build, source, imageName]) ->
|
||||
tmpNameAsync()
|
||||
.then (bufferFile) ->
|
||||
# look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application
|
||||
delete options.application
|
||||
|
||||
# Setup the build args for how the build routine expects them
|
||||
options = _.assign({}, options, { appName })
|
||||
params = _.assign({}, params, { source })
|
||||
Promise.resolve(validateComposeOptions(sdk, options))
|
||||
.then ->
|
||||
if not appName?
|
||||
throw new Error('Please specify the name of the application to deploy')
|
||||
|
||||
Promise.try ->
|
||||
if build
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, logger)
|
||||
else
|
||||
{ image: imageName, log: '' }
|
||||
.then ({ image: imageName, log: buildLogs }) ->
|
||||
logger.logInfo('Initializing deploy...')
|
||||
if image? and options.build
|
||||
throw new Error('Build option is not applicable when specifying an image')
|
||||
|
||||
logs = buildLogs
|
||||
Promise.all [
|
||||
dockerUtils.bufferImage(docker, imageName, bufferFile)
|
||||
token
|
||||
username
|
||||
url
|
||||
params.appName
|
||||
logger
|
||||
]
|
||||
.spread(performUpload)
|
||||
.finally ->
|
||||
# If the file was never written to (for instance because an error
|
||||
# has occured before any data was written) this call will throw an
|
||||
# ugly error, just suppress it
|
||||
Promise.try ->
|
||||
require('mz/fs').unlink(bufferFile)
|
||||
.catch(_.noop)
|
||||
.tap ({ image: imageName, buildId }) ->
|
||||
logger.logSuccess("Successfully deployed image: #{formatImageName(imageName)}")
|
||||
return buildId
|
||||
.then ({ image: imageName, buildId }) ->
|
||||
if logs is '' or options.nologupload?
|
||||
return ''
|
||||
Promise.join(
|
||||
helpers.getApplication(appName)
|
||||
helpers.getArchAndDeviceType(appName)
|
||||
(app, { arch, device_type }) ->
|
||||
app.arch = arch
|
||||
app.device_type = device_type
|
||||
return app
|
||||
)
|
||||
.then (app) ->
|
||||
[ app, image, !!options.build, !options.nologupload ]
|
||||
|
||||
logger.logInfo('Uploading logs to dashboard...')
|
||||
|
||||
Promise.join(
|
||||
logs
|
||||
token
|
||||
url
|
||||
buildId
|
||||
username
|
||||
params.appName
|
||||
uploadLogs
|
||||
)
|
||||
.return('Successfully uploaded logs')
|
||||
.then (msg) ->
|
||||
logger.logSuccess(msg) if msg isnt ''
|
||||
.asCallback(done)
|
||||
|
||||
Promise.join(
|
||||
resin.auth.getToken()
|
||||
resin.auth.whoami()
|
||||
resin.settings.get('resinUrl')
|
||||
upload
|
||||
)
|
||||
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app
|
||||
appName # may be prefixed by 'owner/', unlike app.app_name
|
||||
image
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -16,6 +16,11 @@ 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'
|
||||
@ -27,33 +32,35 @@ exports.list =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices
|
||||
$ resin devices --application MyApp
|
||||
$ resin devices --app MyApp
|
||||
$ resin devices -a MyApp
|
||||
$ 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')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.device.getAllByApplication(options.application)
|
||||
return resin.models.device.getAll()
|
||||
return 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'
|
||||
'name'
|
||||
'device_name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
@ -72,21 +79,25 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device 7cf02a6
|
||||
$ balena device 7cf02a6
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
|
||||
resin.models.device.getStatus(device).then (status) ->
|
||||
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.name}$"
|
||||
"$#{device.device_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
@ -104,40 +115,16 @@ exports.info =
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.supported =
|
||||
signature: 'devices supported'
|
||||
description: 'list all supported devices'
|
||||
help: '''
|
||||
Use this command to get the list of all supported devices
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices supported
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.config.getDeviceTypes().then (deviceTypes) ->
|
||||
console.log visuals.table.horizontal deviceTypes, [
|
||||
'slug'
|
||||
'name'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.register =
|
||||
signature: 'device register <application>'
|
||||
description: 'register a device'
|
||||
help: '''
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device register MyApp
|
||||
$ resin device register MyApp --uuid <uuid>
|
||||
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -147,23 +134,17 @@ exports.register =
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
commandOptions.optionalDeviceApiKey
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.join(
|
||||
resin.models.application.get(params.application)
|
||||
options.uuid ? resin.models.device.generateUniqueKey()
|
||||
options.deviceApiKey ? resin.models.device.generateUniqueKey()
|
||||
(application, uuid, deviceApiKey) ->
|
||||
balena.models.application.get(params.application)
|
||||
options.uuid ? balena.models.device.generateUniqueKey()
|
||||
(application, uuid) ->
|
||||
console.info("Registering to #{application.app_name}: #{uuid}")
|
||||
if not options.deviceApiKey?
|
||||
console.info("Using generated device api key: #{deviceApiKey}")
|
||||
else
|
||||
console.info('Using provided device api key')
|
||||
return resin.models.device.register(application.id, uuid, deviceApiKey)
|
||||
return balena.models.device.register(application.id, uuid)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
@ -172,24 +153,25 @@ exports.remove =
|
||||
signature: 'device rm <uuid>'
|
||||
description: 'remove a device'
|
||||
help: '''
|
||||
Use this command to remove a device from resin.io.
|
||||
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:
|
||||
|
||||
$ resin device rm 7cf02a6
|
||||
$ resin device rm 7cf02a6 --yes
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
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 ->
|
||||
resin.models.device.remove(params.uuid)
|
||||
balena.models.device.remove(params.uuid)
|
||||
.nodeify(done)
|
||||
|
||||
exports.identify =
|
||||
@ -202,12 +184,13 @@ exports.identify =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device identify 23c73a1
|
||||
$ balena device identify 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.identify(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
signature: 'device reboot <uuid>'
|
||||
@ -217,13 +200,14 @@ exports.reboot =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device reboot 23c73a1
|
||||
$ balena device reboot 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.shutdown =
|
||||
signature: 'device shutdown <uuid>'
|
||||
@ -233,13 +217,14 @@ exports.shutdown =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device shutdown 23c73a1
|
||||
$ balena device shutdown 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.shutdown(params.uuid, options).nodeify(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>'
|
||||
@ -249,12 +234,13 @@ exports.enableDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url enable 23c73a1
|
||||
$ balena device public-url enable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
signature: 'device public-url disable <uuid>'
|
||||
@ -264,12 +250,13 @@ exports.disableDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url disable 23c73a1
|
||||
$ balena device public-url disable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
signature: 'device public-url <uuid>'
|
||||
@ -279,12 +266,13 @@ exports.getDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url 23c73a1
|
||||
$ balena device public-url 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
|
||||
@ -296,18 +284,19 @@ exports.hasDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url status 23c73a1
|
||||
$ balena device public-url status 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
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 resin device'
|
||||
description: 'rename a balena device'
|
||||
help: '''
|
||||
Use this command to rename a device.
|
||||
|
||||
@ -315,13 +304,14 @@ exports.rename =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device rename 7cf02a6
|
||||
$ resin device rename 7cf02a6 MyPi
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
@ -331,7 +321,7 @@ exports.rename =
|
||||
message: 'How do you want to name this device?'
|
||||
type: 'input'
|
||||
|
||||
.then(_.partial(resin.models.device.rename, params.uuid))
|
||||
.then(_.partial(balena.models.device.rename, params.uuid))
|
||||
.nodeify(done)
|
||||
|
||||
exports.move =
|
||||
@ -344,30 +334,42 @@ exports.move =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device move 7cf02a6
|
||||
$ resin device move 7cf02a6 --application MyNewApp
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6 --application MyNewApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
return options.application or patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
application.device_type is device.device_type
|
||||
device.application_name isnt application.app_name
|
||||
]
|
||||
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 resin.models.device.move(params.uuid, 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 resinOS'
|
||||
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.
|
||||
|
||||
@ -376,71 +378,74 @@ exports.init =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device init
|
||||
$ resin device init --application MyApp
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.yes
|
||||
commandOptions.advancedConfig
|
||||
_.assign({}, commandOptions.osVersion, { signature: 'os-version', parameter: 'os-version' })
|
||||
_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
|
||||
commandOptions.drive
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin os build-config`'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
rimraf = Promise.promisify(require('rimraf'))
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
helpers = require('../utils/helpers')
|
||||
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(resin.models.application.get)
|
||||
.then(balena.models.application.get)
|
||||
.then (application) ->
|
||||
|
||||
download = ->
|
||||
tmpNameAsync().then (tempPath) ->
|
||||
osVersion = options['os-version'] or 'default'
|
||||
capitanoRunAsync("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
capitanoRunAsync("device register #{application.app_name}")
|
||||
.then(resin.models.device.get)
|
||||
runCommand("device register #{application.app_name}")
|
||||
.then(balena.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' #{device.uuid}"
|
||||
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
|
||||
if options.config
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
capitanoRunAsync(configureCommand)
|
||||
runCommand(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
capitanoRunAsync(osInitCommand)
|
||||
runCommand(osInitCommand)
|
||||
# Make sure the device resource is removed if there is an
|
||||
# error when configuring or initializing a device image
|
||||
.catch (error) ->
|
||||
resin.models.device.remove(device.uuid).finally ->
|
||||
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
|
||||
|
140
lib/actions/device_ts.ts
Normal file
140
lib/actions/device_ts.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
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 { Device } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
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 {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export type Options = commandOptions.OptionalOsVersionOption &
|
||||
commandOptions.YesOption;
|
||||
}
|
||||
|
||||
export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
|
||||
signature: 'device os-update <uuid>',
|
||||
description: 'Start a Host OS update for a device',
|
||||
help: stripIndent`
|
||||
Use this command to trigger a Host OS update for a device.
|
||||
|
||||
Notice this command will ask for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device os-update 23c73a1
|
||||
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
|
||||
`,
|
||||
options: [commandOptions.optionalOsVersion, commandOptions.yes],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = await import('balena-sdk');
|
||||
const _ = await import('lodash');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
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;
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
@ -1,182 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.list =
|
||||
signature: 'envs'
|
||||
description: 'list all environment variables'
|
||||
help: '''
|
||||
Use this command to list all environment variables for
|
||||
a particular application or device.
|
||||
|
||||
This command lists all custom environment variables.
|
||||
If you want to see all environment variables, including private
|
||||
ones used by resin, use the verbose option.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin envs --application MyApp
|
||||
$ resin envs --application MyApp --verbose
|
||||
$ resin envs --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
|
||||
{
|
||||
signature: 'verbose'
|
||||
description: 'show private environment variables'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.environmentVariables.getAllByApplication(options.application)
|
||||
else if options.device?
|
||||
return resin.models.environmentVariables.device.getAll(options.device)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
|
||||
.tap (environmentVariables) ->
|
||||
if _.isEmpty(environmentVariables)
|
||||
throw new Error('No environment variables found')
|
||||
if not options.verbose
|
||||
isSystemVariable = resin.models.environmentVariables.isSystemVariable
|
||||
environmentVariables = _.reject(environmentVariables, isSystemVariable)
|
||||
|
||||
console.log visuals.table.horizontal environmentVariables, [
|
||||
'id'
|
||||
'name'
|
||||
'value'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'env rm <id>'
|
||||
description: 'remove an environment variable'
|
||||
help: '''
|
||||
Use this command to remove an environment variable from an application.
|
||||
|
||||
Don't remove resin specific variables, as things might not work as expected.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
If you want to eliminate a device environment variable, pass the `--device` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rm 215
|
||||
$ resin env rm 215 --yes
|
||||
$ resin env rm 215 --device
|
||||
'''
|
||||
options: [
|
||||
commandOptions.yes
|
||||
commandOptions.booleanDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.remove(params.id)
|
||||
else
|
||||
resin.models.environmentVariables.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'env add <key> [value]'
|
||||
description: 'add an environment variable'
|
||||
help: '''
|
||||
Use this command to add an enviroment variable to an application.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
Use the `--device` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use `--quiet` to remove it.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env add EDITOR vim --application MyApp
|
||||
$ resin env add TERM --application MyApp
|
||||
$ resin env add EDITOR vim --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if not params.value?
|
||||
params.value = process.env[params.key]
|
||||
|
||||
if not params.value?
|
||||
throw new Error("Environment value not found for key: #{params.key}")
|
||||
else
|
||||
console.info("Warning: using #{params.key}=#{params.value} from host environment")
|
||||
|
||||
if options.application?
|
||||
resin.models.environmentVariables.create(options.application, params.key, params.value)
|
||||
else if options.device?
|
||||
resin.models.environmentVariables.device.create(options.device, params.key, params.value)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'env rename <id> <value>'
|
||||
description: 'rename an environment variable'
|
||||
help: '''
|
||||
Use this command to rename an enviroment variable from an application.
|
||||
|
||||
Pass the `--device` boolean option if you want to rename a device environment variable.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rename 376 emacs
|
||||
$ resin env rename 376 emacs --device
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.booleanDevice ]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.update(params.id, params.value)
|
||||
else
|
||||
resin.models.environmentVariables.update(params.id, params.value)
|
||||
.nodeify(done)
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -17,10 +17,14 @@ 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 _.fromPairs _.map object, (item) ->
|
||||
return _.map object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
@ -39,13 +43,32 @@ indent = (text) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (data) ->
|
||||
console.log indent columnify data,
|
||||
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: resin [COMMAND] [OPTIONS]\n')
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
|
||||
console.log(messages.reachingOut)
|
||||
console.log('\nPrimary commands:\n')
|
||||
|
||||
@ -55,36 +78,39 @@ general = (params, options, done) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.plugin
|
||||
return 'plugins'
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
|
||||
print(parse(groupedCommands.primary))
|
||||
print parse(groupedCommands.primary).sort(getManualSortCompareFunction(
|
||||
manuallySortedPrimaryCommands,
|
||||
([signature, description], manualItem) ->
|
||||
signature == manualItem or signature.startsWith("#{manualItem} ")
|
||||
))
|
||||
|
||||
if options.verbose
|
||||
if not _.isEmpty(groupedCommands.plugins)
|
||||
console.log('\nInstalled plugins:\n')
|
||||
print(parse(groupedCommands.plugins))
|
||||
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
secondaryCommandPromise = getOclifHelpLinePairs()
|
||||
.then (oclifHelpLinePairs) ->
|
||||
print parse(groupedCommands.secondary).concat(oclifHelpLinePairs).sort()
|
||||
else
|
||||
console.log('\nRun `resin help --verbose` to list additional commands')
|
||||
console.log('\nRun `balena help --verbose` to list additional commands')
|
||||
secondaryCommandPromise = Promise.resolve()
|
||||
|
||||
if not _.isEmpty(capitano.state.globalOptions)
|
||||
console.log('\nGlobal Options:\n')
|
||||
print(parse(capitano.state.globalOptions))
|
||||
|
||||
return done()
|
||||
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()
|
||||
return done(new Error("Command not found: #{params.command}"))
|
||||
exitWithExpectedError("Command not found: #{params.command}")
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
@ -95,7 +121,7 @@ command = (params, options, done) ->
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
console.log('\nOptions:\n')
|
||||
print(parse(command.options))
|
||||
print parse(command.options).sort()
|
||||
|
||||
return done()
|
||||
|
||||
@ -107,8 +133,8 @@ exports.help =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin help apps
|
||||
$ resin help os download
|
||||
$ balena help apps
|
||||
$ balena help os download
|
||||
'''
|
||||
primary: true
|
||||
options: [
|
||||
|
53
lib/actions/help_ts.ts
Normal file
53
lib/actions/help_ts.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from '@oclif/command';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { capitanoizeOclifUsage } from '../utils/oclif-utils';
|
||||
|
||||
export async function getOclifHelpLinePairs(): Promise<
|
||||
Array<[string, string]>
|
||||
> {
|
||||
const { convertedCommands } = await import('../preparser');
|
||||
const cmdClasses: Array<Promise<typeof Command>> = [];
|
||||
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)));
|
||||
}
|
||||
return Bluebird.map(cmdClasses, getCmdUsageDescriptionLinePair);
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmdModule: any): [string, string] {
|
||||
const cmd: typeof Command = cmdModule.default;
|
||||
const usage = capitanoizeOclifUsage(cmd.usage);
|
||||
let description = '';
|
||||
// note: [^] matches any characters (including line breaks), achieving the
|
||||
// same effect as the 's' regex flag which is only supported by Node 9+
|
||||
const matches = /\s*([^]+?)\n[^]*/.exec(cmd.description || '');
|
||||
if (matches && matches.length > 1) {
|
||||
description = _.lowerFirst(_.trimEnd(matches[1], '.'));
|
||||
}
|
||||
return [usage, description];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -15,24 +15,27 @@ limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
wizard: require('./wizard')
|
||||
apiKey: require('./api-key')
|
||||
app: require('./app')
|
||||
info: require('./info')
|
||||
auth: require('./auth')
|
||||
device: require('./device')
|
||||
env: require('./environment-variables')
|
||||
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')
|
||||
sync: require('./sync')
|
||||
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')
|
||||
|
@ -1,26 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.version =
|
||||
signature: 'version'
|
||||
description: 'output the version number'
|
||||
help: '''
|
||||
Display the Resin CLI version.
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
packageJSON = require('../../package.json')
|
||||
console.log(packageJSON.version)
|
||||
return done()
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -21,17 +21,36 @@ 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 `resin os initialize <image>` instead.
|
||||
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('resin-device-init')
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
return Promise.try ->
|
||||
config = JSON.parse(params.config)
|
||||
init.initialize(params.image, params.type, config)
|
||||
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)
|
||||
|
74
lib/actions/join.ts
Normal file
74
lib/actions/join.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
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,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -24,14 +24,14 @@ exports.list =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin keys
|
||||
$ balena keys
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.getAll().then (keys) ->
|
||||
balena.models.key.getAll().then (keys) ->
|
||||
console.log visuals.table.horizontal keys, [
|
||||
'id'
|
||||
'title'
|
||||
@ -46,14 +46,14 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin key 17
|
||||
$ balena key 17
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.get(params.id).then (key) ->
|
||||
balena.models.key.get(params.id).then (key) ->
|
||||
console.log visuals.table.vertical key, [
|
||||
'id'
|
||||
'title'
|
||||
@ -61,7 +61,7 @@ exports.info =
|
||||
|
||||
# Since the public key string is long, it might
|
||||
# wrap to lines below, causing the table layout to break.
|
||||
# See https://github.com/resin-io/resin-cli/issues/151
|
||||
# See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key)
|
||||
.nodeify(done)
|
||||
|
||||
@ -69,29 +69,29 @@ exports.remove =
|
||||
signature: 'key rm <id>'
|
||||
description: 'remove a ssh key'
|
||||
help: '''
|
||||
Use this command to remove a SSH key from resin.io.
|
||||
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:
|
||||
|
||||
$ resin key rm 17
|
||||
$ resin key rm 17 --yes
|
||||
$ balena key rm 17
|
||||
$ balena key rm 17 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
|
||||
resin.models.key.remove(params.id)
|
||||
balena.models.key.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'key add <name> [path]'
|
||||
description: 'add a SSH key to resin.io'
|
||||
description: 'add a SSH key to balena'
|
||||
help: '''
|
||||
Use this command to associate a new SSH key with your account.
|
||||
|
||||
@ -100,8 +100,8 @@ exports.add =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | resin key add Main
|
||||
$ balena key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | balena key add Main
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
@ -109,7 +109,7 @@ exports.add =
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
capitano = require('capitano')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.try ->
|
||||
return readFileAsync(params.path, encoding: 'utf8') if params.path?
|
||||
@ -119,5 +119,5 @@ exports.add =
|
||||
capitano.utils.getStdin (data) ->
|
||||
return callback(null, data)
|
||||
|
||||
.then(_.partial(resin.models.key.create, params.name))
|
||||
.then(_.partial(balena.models.key.create, params.name))
|
||||
.nodeify(done)
|
||||
|
59
lib/actions/leave.ts
Normal file
59
lib/actions/leave.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
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,19 +1,21 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
Docker = require('docker-toolbelt')
|
||||
form = require('resin-cli-form')
|
||||
chalk = require('chalk')
|
||||
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
exports.dockerPort = dockerPort = 2375
|
||||
exports.dockerTimeout = dockerTimeout = 2000
|
||||
|
||||
exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container) ->
|
||||
for name in container.Names
|
||||
return false if name.includes('resin_supervisor')
|
||||
return false if (name.includes('resin_supervisor') or name.includes('balena_supervisor'))
|
||||
return true
|
||||
|
||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
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)
|
||||
@ -22,23 +24,22 @@ exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor =
|
||||
filterOutSupervisorContainer(container)
|
||||
.then (containers) ->
|
||||
if _.isEmpty(containers)
|
||||
throw new Error("No containers found in #{deviceIp}")
|
||||
exitWithExpectedError("No containers found in #{deviceIp}")
|
||||
|
||||
return form.ask
|
||||
message: 'Select a container'
|
||||
type: 'list'
|
||||
choices: _.map containers, (container) ->
|
||||
containerName = container.Names[0] or 'Untitled'
|
||||
containerName = container.Names?[0] or 'Untitled'
|
||||
shortContainerId = ('' + container.Id).substr(0, 11)
|
||||
containerStatus = container.Status
|
||||
|
||||
return {
|
||||
name: "#{containerName} (#{shortContainerId}) - #{containerStatus}"
|
||||
name: "#{containerName} (#{shortContainerId})"
|
||||
value: container.Id
|
||||
}
|
||||
|
||||
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort)
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
|
||||
|
||||
container = docker.getContainer(name)
|
||||
container.inspectAsync()
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = { primary: 1 }
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
@ -51,6 +51,8 @@ getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
type: 'ini'
|
||||
location:
|
||||
path: CONNECTIONS_FOLDER.slice(1)
|
||||
# Reconfix still uses the older resin-image-fs, so still needs an
|
||||
# object-based partition definition.
|
||||
partition: BOOT_PARTITION
|
||||
config_json:
|
||||
type: 'json'
|
||||
@ -151,7 +153,7 @@ prepareConnectionFile = (target) ->
|
||||
if _.includes(files, 'resin-wifi')
|
||||
return null
|
||||
|
||||
# Fresh image, new mode, accoding to https://github.com/resin-os/meta-resin/pull/770/files
|
||||
# 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
|
||||
@ -191,18 +193,19 @@ removeHostname = (schema) ->
|
||||
|
||||
module.exports =
|
||||
signature: 'local configure <target>'
|
||||
description: '(Re)configure a resinOS drive or image'
|
||||
description: '(Re)configure a balenaOS drive or image'
|
||||
help: '''
|
||||
Use this command to configure or reconfigure a resinOS drive or image.
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local configure /dev/sdc
|
||||
$ resin local configure path/to/image.img
|
||||
$ 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)
|
||||
@ -215,7 +218,12 @@ module.exports =
|
||||
return if not isMounted
|
||||
umountAsync(params.target)
|
||||
.then (configurationSchema) ->
|
||||
denymount params.target, (cb) ->
|
||||
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) ->
|
||||
@ -223,6 +231,7 @@ module.exports =
|
||||
removeHostname(configurationSchema)
|
||||
reconfix.writeConfiguration(configurationSchema, answers, params.target)
|
||||
.asCallback(cb)
|
||||
denymount params.target, dmHandler, dmOpts
|
||||
.then ->
|
||||
console.log('Done!')
|
||||
.asCallback(done)
|
||||
|
@ -1,120 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'local flash <image>'
|
||||
description: 'Flash an image to a drive'
|
||||
help: '''
|
||||
Use this command to flash a resinOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local flash path/to/resinos.img
|
||||
$ resin local flash path/to/resinos.img --drive /dev/disk2
|
||||
$ resin local flash path/to/resinos.img --drive /dev/disk2 --yes
|
||||
'''
|
||||
options: [
|
||||
signature: 'yes'
|
||||
boolean: true
|
||||
description: 'confirm non-interactively'
|
||||
alias: 'y'
|
||||
,
|
||||
signature: 'drive'
|
||||
parameter: 'drive'
|
||||
description: 'drive'
|
||||
alias: 'd'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
|
||||
_ = require('lodash')
|
||||
os = require('os')
|
||||
Promise = require('bluebird')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
driveListAsync = Promise.promisify(require('drivelist').list)
|
||||
chalk = require('chalk')
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
imageWrite = require('etcher-image-write')
|
||||
|
||||
form.run [
|
||||
{
|
||||
message: 'Select drive'
|
||||
type: 'drive'
|
||||
name: 'drive'
|
||||
},
|
||||
{
|
||||
message: 'This will erase the selected drive. Are you sure?'
|
||||
type: 'confirm'
|
||||
name: 'yes'
|
||||
default: false
|
||||
}
|
||||
],
|
||||
override:
|
||||
drive: options.drive
|
||||
|
||||
# If `options.yes` is `false`, pass `undefined`,
|
||||
# otherwise the question will not be asked because
|
||||
# `false` is a defined value.
|
||||
yes: options.yes || undefined
|
||||
|
||||
# TODO: dedupe with the resin-device-operations
|
||||
.then (answers) ->
|
||||
if answers.yes isnt true
|
||||
console.log(chalk.red.bold('Aborted image flash'))
|
||||
process.exit(0)
|
||||
|
||||
driveListAsync().then (drives) ->
|
||||
selectedDrive = _.find(drives, device: answers.drive)
|
||||
|
||||
if not selectedDrive?
|
||||
throw new Error("Drive not found: #{answers.drive}")
|
||||
|
||||
return selectedDrive
|
||||
.then (selectedDrive) ->
|
||||
progressBars =
|
||||
write: new visuals.Progress('Flashing')
|
||||
check: new visuals.Progress('Validating')
|
||||
|
||||
umountAsync(selectedDrive.device).then ->
|
||||
Promise.props
|
||||
imageSize: fs.statAsync(params.image).get('size'),
|
||||
imageStream: Promise.resolve(fs.createReadStream(params.image))
|
||||
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
|
||||
.then (results) ->
|
||||
imageWrite.write
|
||||
fd: results.driveFileDescriptor
|
||||
device: selectedDrive.raw
|
||||
size: selectedDrive.size
|
||||
,
|
||||
stream: results.imageStream,
|
||||
size: results.imageSize
|
||||
,
|
||||
check: true
|
||||
.then (writer) ->
|
||||
new Promise (resolve, reject) ->
|
||||
writer.on 'progress', (state) ->
|
||||
progressBars[state.type].update(state)
|
||||
writer.on('error', reject)
|
||||
writer.on('done', resolve)
|
||||
.then ->
|
||||
if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
return umountAsync(selectedDrive.device)
|
||||
.asCallback(done)
|
120
lib/actions/local/flash.ts
Normal file
120
lib/actions/local/flash.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as SDK from 'etcher-sdk';
|
||||
|
||||
async function getDrive(options: {
|
||||
drive?: string;
|
||||
}): Promise<SDK.sourceDestination.BlockDevice> {
|
||||
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);
|
||||
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();
|
||||
}
|
||||
scanner.stop();
|
||||
return drive;
|
||||
}
|
||||
|
||||
export const flash: CommandDefinition<
|
||||
{ image: string },
|
||||
{ drive: string; yes: boolean }
|
||||
> = {
|
||||
signature: 'local flash <image>',
|
||||
description: 'Flash an image to a drive',
|
||||
help: stripIndent`
|
||||
Use this command to flash a balenaOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local flash path/to/balenaos.img[.zip|.gz|.bz2|.xz]
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'yes',
|
||||
boolean: true,
|
||||
description: 'confirm non-interactively',
|
||||
alias: 'y',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
parameter: 'drive',
|
||||
description: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
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');
|
||||
|
||||
const drive = await getDrive(options);
|
||||
|
||||
const yes =
|
||||
options.yes ||
|
||||
(await form.ask({
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false,
|
||||
}));
|
||||
if (yes !== true) {
|
||||
console.log(chalk.red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const file = new sourceDestination.File(
|
||||
params.image,
|
||||
sourceDestination.File.OpenFlags.Read,
|
||||
);
|
||||
const source = await file.getInnerSource();
|
||||
|
||||
const progressBars: { [key: string]: any } = {
|
||||
flashing: new visuals.Progress('Flashing'),
|
||||
verifying: new visuals.Progress('Validating'),
|
||||
};
|
||||
|
||||
await multiWrite.pipeSourceToDestinations(
|
||||
source,
|
||||
[drive],
|
||||
(_, error) => {
|
||||
// onFail
|
||||
console.log(chalk.red.bold(error.message));
|
||||
},
|
||||
(progress: SDK.multiWrite.MultiDestinationProgress) => {
|
||||
// onProgress
|
||||
progressBars[progress.type].update(progress);
|
||||
},
|
||||
true, // verify
|
||||
);
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -15,9 +15,4 @@ limitations under the License.
|
||||
###
|
||||
|
||||
exports.configure = require('./configure')
|
||||
exports.flash = require('./flash')
|
||||
exports.logs = require('./logs')
|
||||
exports.scan = require('./scan')
|
||||
exports.ssh = require('./ssh')
|
||||
exports.push = require('./push')
|
||||
exports.stop = require('./stop')
|
||||
exports.flash = require('./flash').flash
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -22,16 +22,16 @@ limitations under the License.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local logs [deviceIp]'
|
||||
description: 'Get or attach to logs of a running container on a resinOS device'
|
||||
description: 'Get or attach to logs of a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local logs
|
||||
$ resin local logs -f
|
||||
$ resin local logs 192.168.1.10
|
||||
$ resin local logs 192.168.1.10 -f
|
||||
$ resin local logs 192.168.1.10 -f --app-name myapp
|
||||
$ 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'
|
||||
@ -47,12 +47,12 @@ module.exports =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('resin-sync')
|
||||
{ forms } = require('balena-sync')
|
||||
{ selectContainerFromDevice, pipeContainerStream } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
if not options['app-name']?
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# Loads '.resin-sync.yml' configuration from 'source' directory.
|
||||
# Loads '.balena-sync.yml' configuration from 'source' directory.
|
||||
# Returns the configuration object on success
|
||||
#
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
resinPush = require('resin-sync').capitano('resin-toolbox')
|
||||
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 `resin local push`.
|
||||
# capitano frontend in `balena local push`.
|
||||
|
||||
resinPushHelp = '''
|
||||
Warning: 'resin local push' requires an openssh-compatible client and 'rsync' to
|
||||
be correctly installed in your shell environment. For more information (including
|
||||
Windows support) please check the README here: https://github.com/resin-io/resin-cli
|
||||
# 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.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
Use this command to push your local changes to a container on a LAN-accessible resinOS device on the fly.
|
||||
'''
|
||||
|
||||
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 'resin local push' the updated settings will be saved in
|
||||
'<source>/.resin-sync.yml' and will be used in later invocations. You can
|
||||
also change any option by editing '.resin-sync.yml' directly.
|
||||
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 '.resin-sync.yml' :
|
||||
Here is an example '.balena-sync.yml' :
|
||||
|
||||
$ cat $PWD/.resin-sync.yml
|
||||
destination: '/usr/src/app'
|
||||
before: 'echo Hello'
|
||||
after: 'echo Done'
|
||||
ignore:
|
||||
- .git
|
||||
- node_modules/
|
||||
$ 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 '.resin-sync.yml'.
|
||||
|
||||
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
|
||||
@ -58,19 +72,24 @@ resinPushHelp = '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local push
|
||||
$ resin local push --app-name test-server --build-triggers package.json,requirements.txt
|
||||
$ resin local push --force-build
|
||||
$ resin local push --force-build --skip-logs
|
||||
$ resin local push --ignore lib/
|
||||
$ resin local push --verbose false
|
||||
$ resin local push 192.168.2.10 --source . --destination /usr/src/app
|
||||
$ resin local push 192.168.2.10 -s /home/user/myResinProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
|
||||
'''
|
||||
$ 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 resinPush,
|
||||
|
||||
module.exports = _.assign balenaPush,
|
||||
signature: 'local push [deviceIp]'
|
||||
help: resinPushHelp
|
||||
primary: true
|
||||
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,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -14,28 +14,34 @@ 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 resinOS device'
|
||||
description: 'Get a shell into a balenaOS device'
|
||||
help: '''
|
||||
Warning: 'resin local ssh' requires an openssh-compatible client to be correctly
|
||||
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/resin-io/resin-cli
|
||||
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 resinOS 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:
|
||||
|
||||
$ resin local ssh
|
||||
$ resin local ssh --host
|
||||
$ resin local ssh --container chaotic_water
|
||||
$ resin local ssh --container chaotic_water --port 22222
|
||||
$ resin local ssh --verbose
|
||||
$ 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'
|
||||
@ -43,11 +49,7 @@ module.exports =
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'host'
|
||||
boolean: true
|
||||
description: 'get a shell into the host OS'
|
||||
alias: 's'
|
||||
,
|
||||
localHostOSAccessOption,
|
||||
signature: 'container'
|
||||
parameter: 'container'
|
||||
default: null
|
||||
@ -64,11 +66,13 @@ module.exports =
|
||||
child_process = require('child_process')
|
||||
Promise = require 'bluebird'
|
||||
_ = require('lodash')
|
||||
{ forms } = require('resin-sync')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if (options.host is true and options.container?)
|
||||
throw new Error('Please pass either --host or --container option')
|
||||
exitWithExpectedError('Please pass either --host or --container option')
|
||||
|
||||
if not options.port?
|
||||
options.port = 22222
|
||||
@ -77,7 +81,7 @@ module.exports =
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (deviceIp) ->
|
||||
_.assign(options, { deviceIp })
|
||||
@ -101,7 +105,8 @@ module.exports =
|
||||
|
||||
if not options.host
|
||||
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
|
||||
command += " docker exec -ti #{container} #{shellCmd}"
|
||||
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,
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -22,16 +22,16 @@ limitations under the License.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local stop [deviceIp]'
|
||||
description: 'Stop a running container on a resinOS device'
|
||||
description: 'Stop a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local stop
|
||||
$ resin local stop --app-name myapp
|
||||
$ resin local stop --all
|
||||
$ resin local stop 192.168.1.10
|
||||
$ resin local stop 192.168.1.10 --app-name myapp
|
||||
$ 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'
|
||||
@ -47,15 +47,15 @@ module.exports =
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
chalk = require('chalk')
|
||||
{ forms, config, ResinLocalDockerUtils } = require('resin-sync')
|
||||
{ forms, config, BalenaLocalDockerUtils } = require('balena-sync')
|
||||
{ selectContainerFromDevice, filterOutSupervisorContainer } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
@docker = new ResinLocalDockerUtils(@deviceIp)
|
||||
@docker = new BalenaLocalDockerUtils(@deviceIp)
|
||||
|
||||
if options.all
|
||||
# Only list running containers
|
||||
@ -67,7 +67,7 @@ module.exports =
|
||||
@docker.stopContainer(Id)
|
||||
|
||||
ymlConfig = config.load()
|
||||
@appName = options['app-name'] ? ymlConfig['local_resinos']?['app-name']
|
||||
@appName = options['app-name'] ? ymlConfig['local_balenaos']?['app-name']
|
||||
@docker.checkForRunningContainer(@appName)
|
||||
.then (isRunning) =>
|
||||
if not isRunning
|
||||
|
@ -1,69 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'logs <uuid>'
|
||||
description: 'show device logs'
|
||||
help: '''
|
||||
Use this command to show logs for a specific device.
|
||||
|
||||
By default, the command prints all log messages and exit.
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the `--tail` option.
|
||||
|
||||
Note that for now you need to provide the whole UUID for this command to work correctly.
|
||||
|
||||
This is due to some technical limitations that we plan to address soon.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logs 23c73a1
|
||||
$ resin logs 23c73a1
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'tail'
|
||||
description: 'continuously stream output'
|
||||
boolean: true
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
moment = require('moment')
|
||||
|
||||
printLine = (line) ->
|
||||
timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)')
|
||||
console.log("#{timestamp} #{line.message}")
|
||||
|
||||
promise = resin.logs.history(params.uuid).each(printLine)
|
||||
|
||||
if not options.tail
|
||||
|
||||
# PubNub keeps the process alive after a history query.
|
||||
# Until this is fixed, we force the process to exit.
|
||||
# This of course prevents this command to be used programatically
|
||||
return promise.catch(done).finally ->
|
||||
process.exit(0)
|
||||
|
||||
promise.then ->
|
||||
resin.logs.subscribe(params.uuid).then (logs) ->
|
||||
logs.on('line', printLine)
|
||||
logs.on('error', done)
|
||||
.catch(done)
|
184
lib/actions/logs.ts
Normal file
184
lib/actions/logs.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
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;
|
||||
},
|
||||
{
|
||||
tail?: boolean;
|
||||
service?: [string] | string;
|
||||
system?: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'logs <uuidOrDevice>',
|
||||
description: 'show device logs',
|
||||
help: stripIndent`
|
||||
Use this command to show logs for a specific device.
|
||||
|
||||
By default, the command prints all log messages and exits.
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the \`--tail\` option.
|
||||
|
||||
If an IP or .local address is passed to this command, logs are displayed from
|
||||
a local mode device with that address. Note that --tail is implied
|
||||
when this command is provided a local mode device.
|
||||
|
||||
Logs from a single service can be displayed with the --service flag. Just system logs
|
||||
can be shown with the --system flag. Note that these flags can be used together.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena logs 23c73a1
|
||||
$ balena logs 23c73a1 --tail
|
||||
|
||||
$ balena logs 192.168.0.31
|
||||
$ balena logs 192.168.0.31 --service my-service
|
||||
$ balena logs 192.168.0.31 --service my-service-1 --service my-service-2
|
||||
|
||||
$ balena logs 23c73a1.local --system
|
||||
$ balena logs 23c73a1.local --system --service my-service`,
|
||||
options: [
|
||||
{
|
||||
signature: 'tail',
|
||||
description: 'continuously stream output',
|
||||
boolean: true,
|
||||
alias: 't',
|
||||
},
|
||||
{
|
||||
signature: 'service',
|
||||
description: stripIndent`
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system or other --service flags.`,
|
||||
parameter: 'service',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'system',
|
||||
alias: 'S',
|
||||
boolean: true,
|
||||
description:
|
||||
'Only show system logs. This can be used in combination with --service.',
|
||||
},
|
||||
],
|
||||
primary: true,
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const isArray = await import('lodash/isArray');
|
||||
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 Logger = await import('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
|
||||
const displayCloudLog = async (line: CloudLog) => {
|
||||
if (!line.isSystem) {
|
||||
let serviceName = await serviceIdToName(balena, line.serviceId);
|
||||
if (serviceName == null) {
|
||||
serviceName = 'Unknown service';
|
||||
}
|
||||
displayLogObject(
|
||||
{ serviceName, ...line },
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
} else {
|
||||
displayLogObject(
|
||||
line,
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
validateIPAddress(params.uuidOrDevice) ||
|
||||
validateDotLocalUrl(params.uuidOrDevice)
|
||||
) {
|
||||
const { DeviceAPI } = await import('../utils/device/api');
|
||||
const deviceApi = new DeviceAPI(logger, params.uuidOrDevice);
|
||||
logger.logDebug('Checking we can access device');
|
||||
try {
|
||||
await deviceApi.ping();
|
||||
} catch (e) {
|
||||
exitWithExpectedError(
|
||||
new Error(
|
||||
`Cannot access local mode device at address ${params.uuidOrDevice}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const logStream = await deviceApi.getLogStream();
|
||||
displayDeviceLogs(
|
||||
logStream,
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
} else {
|
||||
await exitIfNotLoggedIn();
|
||||
if (options.tail) {
|
||||
return balena.logs
|
||||
.subscribe(params.uuidOrDevice, { count: 100 })
|
||||
.then(function(logStream) {
|
||||
logStream.on('line', displayCloudLog);
|
||||
logStream.on('error', done);
|
||||
})
|
||||
.catch(done);
|
||||
} else {
|
||||
return balena.logs
|
||||
.history(params.uuidOrDevice)
|
||||
.each(displayCloudLog)
|
||||
.catch(done);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -14,6 +14,8 @@ 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'
|
||||
@ -22,12 +24,12 @@ exports.set =
|
||||
|
||||
If note command isn't passed, the tool attempts to read from `stdin`.
|
||||
|
||||
To view the notes, use $ resin device <uuid>.
|
||||
To view the notes, use $ balena device <uuid>.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | resin note --device 7cf02a6
|
||||
$ balena note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | balena note --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
signature: 'device'
|
||||
@ -38,13 +40,16 @@ exports.set =
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if _.isEmpty(params.note)
|
||||
throw new Error('Missing note content')
|
||||
exitWithExpectedError('Missing note content')
|
||||
|
||||
resin.models.device.note(options.device, params.note)
|
||||
balena.models.device.note(options.device, params.note)
|
||||
.nodeify(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -30,9 +30,9 @@ resolveVersion = (deviceType, version) ->
|
||||
return Promise.resolve(version)
|
||||
|
||||
form = require('resin-cli-form')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(deviceType)
|
||||
balena.models.os.getSupportedVersions(deviceType)
|
||||
.then ({ versions, recommended }) ->
|
||||
choices = versions.map (v) ->
|
||||
value: v
|
||||
@ -46,19 +46,19 @@ resolveVersion = (deviceType, version) ->
|
||||
|
||||
exports.versions =
|
||||
signature: 'os versions <type>'
|
||||
description: 'show the available resinOS versions for the given device type'
|
||||
description: 'show the available balenaOS versions for the given device type'
|
||||
help: '''
|
||||
Use this command to show the available resinOS versions for a certain device type.
|
||||
Check available types with `resin devices supported`
|
||||
Use this command to show the available balenaOS versions for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
Example:
|
||||
|
||||
$ resin os versions raspberrypi3
|
||||
$ balena os versions raspberrypi3
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(params.type)
|
||||
balena.models.os.getSupportedVersions(params.type)
|
||||
.then ({ versions, recommended }) ->
|
||||
versions.forEach (v) ->
|
||||
console.log(formatVersion(v, v is recommended))
|
||||
@ -68,7 +68,7 @@ exports.download =
|
||||
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 `resin devices supported`
|
||||
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
|
||||
@ -79,12 +79,12 @@ exports.download =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
$ 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: [
|
||||
@ -95,14 +95,14 @@ exports.download =
|
||||
alias: 'o'
|
||||
required: 'You have to specify the output location'
|
||||
}
|
||||
commandOptions.osVersion
|
||||
commandOptions.osVersionOrSemver
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
unzip = require('unzip2')
|
||||
fs = require('fs')
|
||||
rindle = require('rindle')
|
||||
manager = require('resin-image-manager')
|
||||
manager = require('balena-image-manager')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
console.info("Getting device operating system for #{params.type}")
|
||||
@ -146,33 +146,39 @@ exports.download =
|
||||
console.info('The image was downloaded successfully')
|
||||
.nodeify(done)
|
||||
|
||||
buildConfig = (image, deviceType, advanced = false) ->
|
||||
buildConfigForDeviceType = (deviceType, advanced = false) ->
|
||||
form = require('resin-cli-form')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getManifest(image, deviceType)
|
||||
.get('options')
|
||||
.then (questions) ->
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
questions = deviceType.options
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
|
||||
return form.run(questions, { override })
|
||||
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 `resin os configure`.
|
||||
Use this command to prebuild the OS config once and skip the interactive part of `balena os configure`.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ resin os configure ../path/rpi3.img 7cf02a6 --config "$(cat rpi3-config.json)"
|
||||
$ 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: [
|
||||
@ -195,95 +201,6 @@ exports.buildConfig =
|
||||
writeFileAsync(options.output, JSON.stringify(answers, null, 4))
|
||||
.nodeify(done)
|
||||
|
||||
exports.configure =
|
||||
signature: 'os configure <image> [uuid] [deviceApiKey]'
|
||||
description: 'configure an os image'
|
||||
help: '''
|
||||
Use this command to configure a previously downloaded operating system image for
|
||||
the specific device or for an application generally.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+.
|
||||
|
||||
This comand still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be remove in a future release.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.advancedConfig
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(fs.readFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
init = require('resin-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
|
||||
if _.filter([
|
||||
options.device
|
||||
options.application
|
||||
params.uuid
|
||||
]).length != 1
|
||||
patterns.expectedError '''
|
||||
To configure an image, you must provide exactly one of:
|
||||
|
||||
* A device, with --device <uuid>
|
||||
* An application, with --app <appname>
|
||||
* [Deprecated] A device, passing its uuid directly on the command line
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ resin help os configure
|
||||
'''
|
||||
if params.uuid
|
||||
console.warn(
|
||||
'Directly passing a UUID to `resin os configure` is deprecated. Pass it with --uuid <uuid> instead.' +
|
||||
if params.deviceApiKey
|
||||
' Device api keys can be passed with --deviceApiKey.\n'
|
||||
else '\n'
|
||||
)
|
||||
|
||||
uuid = options.device || params.uuid
|
||||
deviceApiKey = options.deviceApiKey || params.deviceApiKey
|
||||
|
||||
console.info('Configuring operating system image')
|
||||
|
||||
configurationResourceType = if uuid then 'device' else 'application'
|
||||
|
||||
resin.models[configurationResourceType].get(uuid || options.application)
|
||||
.then (appOrDevice) ->
|
||||
Promise.try ->
|
||||
if options.config
|
||||
return readFileAsync(options.config, 'utf8')
|
||||
.then(JSON.parse)
|
||||
return buildConfig(params.image, appOrDevice.device_type, options.advanced)
|
||||
.then (answers) ->
|
||||
(if configurationResourceType == 'device'
|
||||
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
|
||||
else
|
||||
generateApplicationConfig(appOrDevice, answers)
|
||||
).then (config) ->
|
||||
init.configure(params.image, appOrDevice.device_type, config, answers)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
INIT_WARNING_MESSAGE = '''
|
||||
Note: Initializing the device may ask for administrative permissions
|
||||
because we need to access the raw devices directly.
|
||||
@ -299,14 +216,14 @@ exports.initialize =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
"""
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.yes
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -325,7 +242,7 @@ exports.initialize =
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
""")
|
||||
helpers.getManifest(params.image, options.type)
|
||||
Promise.resolve(helpers.getManifest(params.image, options.type))
|
||||
.then (manifest) ->
|
||||
return manifest.initialization?.options
|
||||
.then (questions) ->
|
||||
@ -338,6 +255,7 @@ exports.initialize =
|
||||
options.yes
|
||||
"This will erase #{answers.drive}. Are you sure?"
|
||||
"Going to erase #{answers.drive}."
|
||||
true
|
||||
)
|
||||
.return(answers.drive)
|
||||
.then(umountAsync)
|
||||
@ -352,7 +270,7 @@ exports.initialize =
|
||||
.then (answers) ->
|
||||
return if not answers.drive?
|
||||
|
||||
# TODO: resin local makes use of ejectAsync, see below
|
||||
# TODO: balena local makes use of ejectAsync, see below
|
||||
# DO we need this / should we do that here?
|
||||
|
||||
# getDrive = (drive) ->
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
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.
|
||||
@ -14,42 +14,70 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
LATEST = 'latest'
|
||||
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('resin-preload')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
preload = require('balena-preload')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
filter:
|
||||
device_type: deviceType
|
||||
build:
|
||||
$any:
|
||||
$alias: 'b'
|
||||
$expr:
|
||||
b:
|
||||
status: 'success'
|
||||
expand: preload.applicationExpandOptions
|
||||
select: [ 'id', 'app_name', 'device_type', 'commit' ]
|
||||
orderby: 'app_name asc'
|
||||
getDeviceTypesWithSameArch(deviceType)
|
||||
.then (deviceTypes) ->
|
||||
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')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and builds.')
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
|
||||
applicationInfoSpinner.start()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds(deviceType)
|
||||
.then (applications) ->
|
||||
applicationInfoSpinner.stop()
|
||||
if applications.length == 0
|
||||
expectedError("You have no apps with successful builds for a '#{deviceType}' device type.")
|
||||
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
|
||||
form.ask
|
||||
message: 'Select an application'
|
||||
type: 'list'
|
||||
@ -57,36 +85,41 @@ selectApplication = (deviceType) ->
|
||||
name: app.app_name
|
||||
value: app
|
||||
|
||||
selectApplicationCommit = (builds) ->
|
||||
selectApplicationCommit = (releases) ->
|
||||
form = require('resin-cli-form')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if builds.length == 0
|
||||
expectedError('This application has no successful builds.')
|
||||
DEFAULT_CHOICE = {'name': LATEST, 'value': LATEST}
|
||||
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
|
||||
name: "#{build.push_timestamp} - #{build.commit_hash}"
|
||||
value: build.commit_hash
|
||||
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 build'
|
||||
message: 'Select a release'
|
||||
type: 'list'
|
||||
default: LATEST
|
||||
default: 'current'
|
||||
choices: choices
|
||||
|
||||
offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
if commit == LATEST or not application.should_track_latest_release
|
||||
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 latest available version.
|
||||
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 latest build once it is online.
|
||||
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,
|
||||
@ -94,69 +127,90 @@ offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
.then (update) ->
|
||||
if not update
|
||||
return
|
||||
resin.pine.patch
|
||||
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: '(beta) preload an app on a disk image (or Edison zip archive)'
|
||||
description: 'preload an app on a disk image (or Edison zip archive)'
|
||||
help: '''
|
||||
Warning: "resin preload" requires Docker to be correctly installed in
|
||||
your shell environment. For more information (including Windows support)
|
||||
please check the README here: https://github.com/resin-io/resin-cli .
|
||||
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.
|
||||
|
||||
Use this command to preload an application to a local disk image (or
|
||||
Edison zip archive) with a built commit from Resin.io.
|
||||
This can be used with cloud builds, or images deployed with resin deploy.
|
||||
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:
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
|
||||
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
|
||||
$ balena preload balena.img
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: dockerUtils.appendConnectionOptions [
|
||||
{
|
||||
signature: 'app'
|
||||
parameter: 'appId'
|
||||
description: 'id of the application to preload'
|
||||
alias: 'a'
|
||||
}
|
||||
{
|
||||
signature: 'commit'
|
||||
parameter: 'hash'
|
||||
description: '''
|
||||
a specific application commit to preload, use "latest" to specify the latest commit
|
||||
(ignored if no appId is given)
|
||||
'''
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'splash-image'
|
||||
parameter: 'splashImage.png'
|
||||
description: 'path to a png image to replace the splash screen'
|
||||
alias: 's'
|
||||
}
|
||||
{
|
||||
signature: 'dont-check-device-type'
|
||||
boolean: true
|
||||
description: 'Disables check for matching device types in image and application'
|
||||
}
|
||||
]
|
||||
options: preloadOptions
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
form = require('resin-cli-form')
|
||||
preload = require('resin-preload')
|
||||
errors = require('resin-errors')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
preload = require('balena-preload')
|
||||
visuals = require('resin-cli-visuals')
|
||||
nodeCleanup = require('node-cleanup')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
progressBars = {}
|
||||
|
||||
@ -178,6 +232,7 @@ module.exports =
|
||||
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
|
||||
@ -185,21 +240,39 @@ module.exports =
|
||||
options.splashImage = options['splash-image']
|
||||
delete options['splash-image']
|
||||
|
||||
if options['dont-check-device-type'] and not options.appId
|
||||
expectedError('You need to specify an app id if you disable the device type check.')
|
||||
options.dontCheckArch = options['dont-check-arch'] || false
|
||||
delete options['dont-check-arch']
|
||||
if options.dontCheckArch and not options.appId
|
||||
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
|
||||
|
||||
options.pinDevice = options['pin-device-to-release'] || false
|
||||
delete options['pin-device-to-release']
|
||||
|
||||
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(
|
||||
resin,
|
||||
docker,
|
||||
options.appId,
|
||||
options.commit,
|
||||
options.image,
|
||||
options.splashImage,
|
||||
options.proxy,
|
||||
balena
|
||||
docker
|
||||
options.appId
|
||||
options.commit
|
||||
options.image
|
||||
options.splashImage
|
||||
options.proxy
|
||||
options.dontCheckArch
|
||||
options.pinDevice
|
||||
certificates
|
||||
)
|
||||
|
||||
gotSignal = false
|
||||
@ -223,51 +296,39 @@ module.exports =
|
||||
return new Promise (resolve, reject) ->
|
||||
preloader.on('error', reject)
|
||||
|
||||
preloader.build()
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.getDeviceTypeAndPreloadedBuilds()
|
||||
.then (info) ->
|
||||
# If no appId was provided, show a list of matching apps
|
||||
Promise.try ->
|
||||
if options.appId
|
||||
return preloader.fetchApplication()
|
||||
.catch(errors.ResinApplicationNotFound, expectedError)
|
||||
selectApplication(info.device_type)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
# Check that the app device type and the image device type match
|
||||
if not options['dont-check-device-type'] and info.device_type != application.device_type
|
||||
expectedError(
|
||||
"Image device type (#{info.device_type}) and application device type (#{application.device_type}) do not match"
|
||||
)
|
||||
if not preloader.appId
|
||||
selectApplication(preloader.config.deviceType)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
.then ->
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if 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
|
||||
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if options.commit == LATEST and application.commit
|
||||
# handle `--commit latest`
|
||||
return LATEST
|
||||
else if not _.find(application.build, commit_hash: options.commit)
|
||||
expectedError('There is no build matching this commit')
|
||||
return options.commit
|
||||
selectApplicationCommit(application.build)
|
||||
.then (commit) ->
|
||||
if commit == LATEST
|
||||
preloader.commit = application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Propose to disable automatic app updates if the commit is not the latest
|
||||
offerToDisableAutomaticUpdates(application, commit)
|
||||
.then ->
|
||||
builds = info.preloaded_builds.map (build) ->
|
||||
build.slice(-preload.BUILD_HASH_LENGTH)
|
||||
if preloader.commit in builds
|
||||
throw new preload.errors.ResinError('This build is already preloaded in this image.')
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(preload.errors.ResinError, expectedError)
|
||||
# Propose to disable automatic app updates if the commit is not the 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)
|
||||
|
381
lib/actions/push.ts
Normal file
381
lib/actions/push.ts
Normal file
@ -0,0 +1,381 @@
|
||||
/*
|
||||
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 { BalenaSDK } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { registrySecretsHelp } from '../utils/messages';
|
||||
import {
|
||||
validateApplicationName,
|
||||
validateDotLocalUrl,
|
||||
validateIPAddress,
|
||||
} from '../utils/validation';
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
Device,
|
||||
}
|
||||
|
||||
function getBuildTarget(appOrDevice: string): BuildTarget | null {
|
||||
// First try the application regex from the api
|
||||
if (validateApplicationName(appOrDevice)) {
|
||||
return BuildTarget.Cloud;
|
||||
}
|
||||
|
||||
if (validateIPAddress(appOrDevice) || validateDotLocalUrl(appOrDevice)) {
|
||||
return BuildTarget.Device;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
const { exitWithExpectedError, selectFromList } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const _ = await import('lodash');
|
||||
|
||||
const applications = await sdk.models.application.getAll({
|
||||
$expand: {
|
||||
user: {
|
||||
$select: ['username'],
|
||||
},
|
||||
},
|
||||
$filter: {
|
||||
$eq: [{ $tolower: { $: 'app_name' } }, appName.toLowerCase()],
|
||||
},
|
||||
$select: ['id'],
|
||||
});
|
||||
|
||||
if (applications == null || applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
No applications found with name: ${appName}.
|
||||
|
||||
This could mean that the application does not exist, or you do
|
||||
not have the permissions required to access it.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (applications.length === 1) {
|
||||
return _.get(applications, '[0].user[0].username');
|
||||
}
|
||||
|
||||
// If we got more than one application with the same name it means that the
|
||||
// user has access to a collab app with the same name as a personal app. We
|
||||
// present a list to the user which shows the fully qualified application
|
||||
// name (user/appname) and allows them to select
|
||||
const entries = _.map(applications, app => {
|
||||
const username = _.get(app, 'user[0].username');
|
||||
return {
|
||||
name: `${username}/${appName}`,
|
||||
extra: username,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await selectFromList(
|
||||
`${
|
||||
entries.length
|
||||
} applications found with that name, please select the application you would like to push to`,
|
||||
entries,
|
||||
);
|
||||
|
||||
return selected.extra;
|
||||
}
|
||||
|
||||
export const push: CommandDefinition<
|
||||
{
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
applicationOrDevice: string;
|
||||
applicationOrDevice_raw: string;
|
||||
},
|
||||
{
|
||||
source?: string;
|
||||
emulated?: boolean;
|
||||
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
|
||||
nocache?: boolean;
|
||||
'registry-secrets'?: string;
|
||||
nolive?: boolean;
|
||||
detached?: boolean;
|
||||
service?: string | string[];
|
||||
system?: boolean;
|
||||
env?: string | string[];
|
||||
}
|
||||
> = {
|
||||
signature: 'push <applicationOrDevice>',
|
||||
primary: true,
|
||||
description:
|
||||
'Start a remote build on the balena cloud build servers or a local mode device',
|
||||
help: stripIndent`
|
||||
This command can be used to start a build on the remote balena cloud builders,
|
||||
or a local mode balena device.
|
||||
|
||||
When building on the balenaCloud servers, the given source directory will be
|
||||
sent to the remote server. This can be used as a drop-in replacement for the
|
||||
"git push" deployment method.
|
||||
|
||||
When building on a local mode device, the given source directory will be
|
||||
built on the device, and the resulting containers will be run on the device.
|
||||
Logs will be streamed back from the device as part of the same invocation.
|
||||
The web dashboard can be used to switch a device to local mode:
|
||||
https://www.balena.io/docs/learn/develop/local-mode/
|
||||
Note that local mode requires a supervisor version of at least v7.21.0.
|
||||
The logs from only a single service can be shown with the --service flag, and
|
||||
showing only the system logs can be achieved with --system. Note that these
|
||||
flags can be used together.
|
||||
|
||||
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
|
||||
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')}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena push myApp
|
||||
$ balena push myApp --source <source directory>
|
||||
$ balena push myApp -s <source directory>
|
||||
|
||||
$ balena push 10.0.0.1
|
||||
$ balena push 10.0.0.1 --source <source directory>
|
||||
$ balena push 10.0.0.1 --service my-service
|
||||
$ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
|
||||
$ balena push 10.0.0.1 --nolive
|
||||
|
||||
$ balena push 23c73a1.local --system
|
||||
$ balena push 23c73a1.local --system --service my-service
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'source',
|
||||
alias: 's',
|
||||
description:
|
||||
'The source that should be sent to the balena builder to be built (defaults to the current directory)',
|
||||
parameter: 'source',
|
||||
},
|
||||
{
|
||||
signature: 'emulated',
|
||||
alias: 'e',
|
||||
description: 'Force an emulated build to occur on the remote builder',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'dockerfile',
|
||||
parameter: 'Dockerfile',
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
},
|
||||
{
|
||||
signature: 'nocache',
|
||||
alias: 'c',
|
||||
description: "Don't use cache when building this project",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'registry-secrets',
|
||||
alias: 'R',
|
||||
parameter: 'secrets.yml|.json',
|
||||
description: stripIndent`
|
||||
Path to a local YAML or JSON file containing Docker registry passwords used to pull base images.
|
||||
Note that if registry-secrets are not provided on the command line, a secrets configuration
|
||||
file from the balena directory will be used (usually $HOME/.balena/secrets.yml|.json)`,
|
||||
},
|
||||
{
|
||||
signature: 'nolive',
|
||||
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
|
||||
and required to cause the process to end once the initial build has completed.`,
|
||||
},
|
||||
{
|
||||
signature: 'detached',
|
||||
alias: 'd',
|
||||
description: stripIndent`
|
||||
When pushing to the cloud, this option will cause the build to start, then return execution
|
||||
back to the shell, with the status and release ID (if applicable).
|
||||
|
||||
When pushing to a local mode device, this option will cause the command to not tail application logs when the build
|
||||
has completed.`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'service',
|
||||
description: stripIndent`
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system and other --service flags.
|
||||
Only valid when pushing to a local mode device.`,
|
||||
parameter: 'service',
|
||||
},
|
||||
{
|
||||
signature: 'system',
|
||||
description: stripIndent`
|
||||
Only show system logs. This can be used in combination with --service.
|
||||
Only valid when pushing to a local mode device.`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'env',
|
||||
parameter: 'env',
|
||||
description: stripIndent`
|
||||
When performing a push to device, run the built containers with environment
|
||||
variables provided with this argument. Environment variables can be applied
|
||||
to individual services by adding their service name before the argument,
|
||||
separated by a colon, e.g:
|
||||
--env main:MY_ENV=value
|
||||
Note that if the service name cannot be found in the composition, the entire
|
||||
left hand side of the = character will be treated as the variable name.
|
||||
`,
|
||||
},
|
||||
],
|
||||
async action(params, options, done) {
|
||||
const sdk = (await import('balena-sdk')).fromSharedOptions();
|
||||
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 { 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');
|
||||
}
|
||||
|
||||
const source = options.source || '.';
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Using ${source} as build source`);
|
||||
}
|
||||
|
||||
const dockerfilePath = validateSpecifiedDockerfile(
|
||||
source,
|
||||
options.dockerfile,
|
||||
);
|
||||
|
||||
const registrySecrets = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
|
||||
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(
|
||||
'The --nolive flag is only valid when pushing to a local mode device',
|
||||
);
|
||||
}
|
||||
if (options.service) {
|
||||
exitWithExpectedError(
|
||||
'The --service flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
if (options.system) {
|
||||
exitWithExpectedError(
|
||||
'The --system flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
if (options.env) {
|
||||
exitWithExpectedError(
|
||||
'The --env flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
|
||||
const app = appOrDevice;
|
||||
await exitIfNotLoggedIn();
|
||||
await Bluebird.join(
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
getAppOwner(sdk, app),
|
||||
async (token, baseUrl, owner) => {
|
||||
const opts = {
|
||||
dockerfilePath,
|
||||
emulated: options.emulated || false,
|
||||
nocache: options.nocache || false,
|
||||
registrySecrets,
|
||||
headless: options.detached || false,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
|
||||
return await remote.startRemoteBuild(args);
|
||||
},
|
||||
).nodeify(done);
|
||||
break;
|
||||
case BuildTarget.Device:
|
||||
const device = appOrDevice;
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
// TODO: Support passing a different port
|
||||
await Bluebird.resolve(
|
||||
deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
nocache: options.nocache || false,
|
||||
nolive: options.nolive || false,
|
||||
detached: options.detached || false,
|
||||
services: servicesToDisplay,
|
||||
system: options.system || false,
|
||||
env:
|
||||
typeof options.env === 'string'
|
||||
? [options.env]
|
||||
: options.env || [],
|
||||
}),
|
||||
)
|
||||
.catch(BuildError, e => {
|
||||
exitWithExpectedError(e.toString());
|
||||
})
|
||||
.nodeify(done);
|
||||
break;
|
||||
default:
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
Build target not recognised. Please provide either an application name or device address.
|
||||
|
||||
The only supported device addresses currently are IP addresses.
|
||||
|
||||
If you believe your build target should have been detected, and this is an error, please
|
||||
create an issue.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
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.
|
||||
@ -33,15 +33,15 @@ dockerVersionProperties = [
|
||||
]
|
||||
|
||||
module.exports =
|
||||
signature: 'local scan'
|
||||
description: 'Scan for resinOS devices in your local network'
|
||||
signature: 'scan'
|
||||
description: 'Scan for balenaOS devices in your local network'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local scan
|
||||
$ resin local scan --timeout 120
|
||||
$ resin local scan --verbose
|
||||
$ balena scan
|
||||
$ balena scan --timeout 120
|
||||
$ balena scan --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
@ -60,30 +60,31 @@ module.exports =
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
prettyjson = require('prettyjson')
|
||||
Docker = require('docker-toolbelt')
|
||||
{ discover } = require('resin-sync')
|
||||
{ discover } = require('balena-sync')
|
||||
{ SpinnerPromise } = require('resin-cli-visuals')
|
||||
{ dockerPort, dockerTimeout } = require('./common')
|
||||
{ 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.discoverLocalResinOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local resinOS devices..'
|
||||
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local balenaOS devices..'
|
||||
stopMessage: 'Reporting scan results'
|
||||
.filter ({ address }) ->
|
||||
Promise.try ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker.pingAsync()
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
.tap (devices) ->
|
||||
if _.isEmpty(devices)
|
||||
throw new Error('Could not find any resinOS devices in the local network')
|
||||
exitWithExpectedError('Could not find any balenaOS devices in the local network')
|
||||
.map ({ host, address }) ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
Promise.props
|
||||
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
|
||||
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
/*
|
||||
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.
|
||||
@ -12,23 +12,28 @@ 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.list =
|
||||
signature: 'settings'
|
||||
description: 'print current settings'
|
||||
help: '''
|
||||
Use this command to display detected settings
|
||||
import { CommandDefinition } from 'capitano';
|
||||
|
||||
Examples:
|
||||
export const list: CommandDefinition = {
|
||||
signature: 'settings',
|
||||
description: 'print current settings',
|
||||
help: `\
|
||||
Use this command to display detected settings
|
||||
|
||||
$ resin settings
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
prettyjson = require('prettyjson')
|
||||
Examples:
|
||||
|
||||
resin.settings.getAll()
|
||||
$ 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)
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
@ -1,130 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'ssh [uuid]'
|
||||
description: '(beta) get a shell into the running app container of a device'
|
||||
help: '''
|
||||
Warning: 'resin 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/resin-io/resin-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin ssh MyApp
|
||||
$ resin ssh 7cf02a6
|
||||
$ resin ssh 7cf02a6 --port 8080
|
||||
$ resin ssh 7cf02a6 -v
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'port'
|
||||
parameter: 'port'
|
||||
description: 'ssh gateway port'
|
||||
alias: 'p'
|
||||
,
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'noproxy'
|
||||
boolean: true
|
||||
description: "don't use the proxy configuration for this connection.
|
||||
Only makes sense if you've configured proxy globally."
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
child_process = require('child_process')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
bash = require('bash')
|
||||
hasbin = require('hasbin')
|
||||
{ getSubShellCommand } = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
options.port ?= 22
|
||||
|
||||
verbose = if options.verbose then '-vvv' else ''
|
||||
|
||||
proxyConfig = global.PROXY_CONFIG
|
||||
useProxy = !!proxyConfig and not options.noproxy
|
||||
|
||||
getSshProxyCommand = (hasTunnelBin) ->
|
||||
return '' if not useProxy
|
||||
|
||||
if not hasTunnelBin
|
||||
console.warn('''
|
||||
Proxy is enabled but the `proxytunnel` binary cannot be found.
|
||||
Please install it if you want to route the `resin ssh` requests through the proxy.
|
||||
Alternatively you can pass `--noproxy` param to the `resin ssh` command to ignore the proxy config
|
||||
for the `ssh` requests.
|
||||
|
||||
Attemmpting the unproxied request for now.
|
||||
''')
|
||||
return ''
|
||||
|
||||
tunnelOptions =
|
||||
proxy: "#{proxyConfig.host}:#{proxyConfig.port}"
|
||||
dest: '%h:%p'
|
||||
{ proxyAuth } = proxyConfig
|
||||
if proxyAuth
|
||||
i = proxyAuth.indexOf(':')
|
||||
_.assign tunnelOptions,
|
||||
user: proxyAuth.substring(0, i)
|
||||
pass: proxyAuth.substring(i + 1)
|
||||
proxyCommand = "proxytunnel #{bash.args(tunnelOptions, '--', '=')}"
|
||||
return "-o #{bash.args({ ProxyCommand: proxyCommand }, '', '=')}"
|
||||
|
||||
Promise.try ->
|
||||
return false if not params.uuid
|
||||
return resin.models.device.has(params.uuid)
|
||||
.then (uuidExists) ->
|
||||
return params.uuid if uuidExists
|
||||
return patterns.inferOrSelectDevice()
|
||||
.then (uuid) ->
|
||||
console.info("Connecting to: #{uuid}")
|
||||
resin.models.device.get(uuid)
|
||||
.then (device) ->
|
||||
throw new Error('Device is not online') if not device.is_online
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
uuid: device.uuid
|
||||
# get full uuid
|
||||
containerId: resin.models.device.getApplicationInfo(device.uuid).get('containerId')
|
||||
proxyUrl: resin.settings.get('proxyUrl')
|
||||
|
||||
hasTunnelBin: if useProxy then hasbin('proxytunnel') else null
|
||||
.then ({ username, uuid, containerId, proxyUrl, hasTunnelBin }) ->
|
||||
throw new Error('Did not find running application container') if not containerId?
|
||||
Promise.try ->
|
||||
sshProxyCommand = getSshProxyCommand(hasTunnelBin)
|
||||
command = "ssh #{verbose} -t \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
#{sshProxyCommand} \
|
||||
-p #{options.port} #{username}@ssh.#{proxyUrl} enter #{uuid} #{containerId}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
stdio: 'inherit'
|
||||
.nodeify(done)
|
372
lib/actions/ssh.ts
Normal file
372
lib/actions/ssh.ts
Normal file
@ -0,0 +1,372 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { BalenaDeviceNotFound } from 'balena-errors';
|
||||
import { validateDotLocalUrl, validateIPAddress } from '../utils/validation';
|
||||
|
||||
async function getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
uuid: string,
|
||||
serviceName: string,
|
||||
sshOpts: {
|
||||
port?: number;
|
||||
proxyCommand?: string;
|
||||
proxyUrl: string;
|
||||
username: string;
|
||||
},
|
||||
version?: string,
|
||||
id?: number,
|
||||
): Promise<string> {
|
||||
const semver = await import('resin-semver');
|
||||
|
||||
if (version == null || id == null) {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version'],
|
||||
});
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
}
|
||||
|
||||
let containerId: string | undefined;
|
||||
if (semver.gte(version, '8.6.0')) {
|
||||
const apiUrl = await sdk.settings.get('apiUrl');
|
||||
// TODO: Move this into the SDKs device model
|
||||
const request = await sdk.request.send({
|
||||
method: 'POST',
|
||||
url: '/supervisor/v2/containerId',
|
||||
baseUrl: apiUrl,
|
||||
body: {
|
||||
method: 'GET',
|
||||
deviceId: id,
|
||||
},
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${
|
||||
request.status
|
||||
}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${
|
||||
body.message
|
||||
}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
} else {
|
||||
console.log(stripIndent`
|
||||
Using legacy method to detect container ID. This will be slow.
|
||||
To speed up this process, please update your device to an OS
|
||||
which has a supervisor version of at least v8.6.0.
|
||||
`);
|
||||
// We need to execute a balena ps command on the device,
|
||||
// and parse the output, looking for a specific
|
||||
// container
|
||||
const { child_process } = await import('mz');
|
||||
const escapeRegex = await import('lodash/escapeRegExp');
|
||||
const { getSubShellCommand } = await import('../utils/helpers');
|
||||
const { deviceContainerEngineBinary } = await import('../utils/device/ssh');
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
verbose: false,
|
||||
port: sshOpts.port,
|
||||
command: `host ${uuid} '"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"'`,
|
||||
proxyCommand: sshOpts.proxyCommand,
|
||||
proxyUrl: sshOpts.proxyUrl,
|
||||
username: sshOpts.username,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
const subprocess = child_process.spawn(
|
||||
subShellCommand.program,
|
||||
subShellCommand.args,
|
||||
{
|
||||
stdio: [null, 'pipe', null],
|
||||
},
|
||||
);
|
||||
const containers = await new Promise<string>((resolve, reject) => {
|
||||
let output = '';
|
||||
subprocess.stdout.on('data', chunk => (output += chunk.toString()));
|
||||
subprocess.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Non-zero error code when looking for service container: ${code}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve(output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lines = containers.split('\n');
|
||||
const regex = new RegExp(`\\/?${escapeRegex(serviceName)}_\\d+_\\d+`);
|
||||
for (const container of lines) {
|
||||
const [cId, name] = container.split(' ');
|
||||
if (regex.test(name)) {
|
||||
containerId = cId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
throw new Error(
|
||||
`Could not find a service ${serviceName} on device ${uuid}.`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
function generateVpnSshCommand(opts: {
|
||||
uuid: string;
|
||||
command: string;
|
||||
verbose: boolean;
|
||||
port?: number;
|
||||
username: string;
|
||||
proxyUrl: string;
|
||||
proxyCommand?: string;
|
||||
}) {
|
||||
return (
|
||||
`ssh ${
|
||||
opts.verbose ? '-vvv' : ''
|
||||
} -t -o LogLevel=ERROR -o StrictHostKeyChecking=no ` +
|
||||
`-o UserKnownHostsFile=/dev/null ` +
|
||||
`${opts.proxyCommand != null ? opts.proxyCommand : ''} ` +
|
||||
`${opts.port != null ? `-p ${opts.port}` : ''} ` +
|
||||
`${opts.username}@ssh.${opts.proxyUrl} ${opts.command}`
|
||||
);
|
||||
}
|
||||
|
||||
export const ssh: CommandDefinition<
|
||||
{
|
||||
applicationOrDevice: string;
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
applicationOrDevice_raw: string;
|
||||
serviceName?: string;
|
||||
},
|
||||
{
|
||||
port: string;
|
||||
service: string;
|
||||
verbose: true | undefined;
|
||||
noProxy: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'ssh <applicationOrDevice> [serviceName]',
|
||||
description: 'SSH into the host or application container of a device',
|
||||
primary: true,
|
||||
help: stripIndent`
|
||||
This command can be used to start a shell on a local or remote device.
|
||||
|
||||
If a service name is not provided, a shell will be opened on the host OS.
|
||||
|
||||
If an application name is provided, all online devices in the application
|
||||
will be presented, and the chosen device will then have a shell opened on
|
||||
in it's service container or host OS.
|
||||
|
||||
For local devices, the IP address and .local domain name are supported.
|
||||
If the device is referenced by IP or \`.local\` address, the connection
|
||||
is initiated directly to balenaOS on port \`22222\` via an
|
||||
openssh-compatible client. Otherwise, any connection initiated remotely
|
||||
traverses the balenaCloud VPN.
|
||||
|
||||
Examples:
|
||||
balena ssh MyApp
|
||||
|
||||
balena ssh f49cefd
|
||||
balena ssh f49cefd my-service
|
||||
balena ssh f49cefd --port <port>
|
||||
|
||||
balena ssh 192.168.0.1 --verbose
|
||||
balena ssh f49cefd.local my-service
|
||||
|
||||
Warning: \`balena ssh\` requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check:
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies`,
|
||||
options: [
|
||||
{
|
||||
signature: 'port',
|
||||
parameter: 'port',
|
||||
description: 'SSH gateway port',
|
||||
alias: 'p',
|
||||
},
|
||||
{
|
||||
signature: 'verbose',
|
||||
boolean: true,
|
||||
description: 'Increase verbosity',
|
||||
alias: 'v',
|
||||
},
|
||||
{
|
||||
signature: 'noproxy',
|
||||
boolean: true,
|
||||
description: stripIndent`
|
||||
Don't use the proxy configuration for this connection. This flag
|
||||
only make sense if you've configured a proxy globally.`,
|
||||
},
|
||||
],
|
||||
action: async (params, options) => {
|
||||
const applicationOrDevice =
|
||||
params.applicationOrDevice_raw || params.applicationOrDevice;
|
||||
const bash = await import('bash');
|
||||
// TODO: Make this typed
|
||||
const hasbin = require('hasbin');
|
||||
const { getSubShellCommand } = await import('../utils/helpers');
|
||||
const { child_process } = await import('mz');
|
||||
const {
|
||||
exitIfNotLoggedIn,
|
||||
exitWithExpectedError,
|
||||
getOnlineTargetUuid,
|
||||
} = await import('../utils/patterns');
|
||||
const sdk = BalenaSdk.fromSharedOptions();
|
||||
|
||||
const verbose = options.verbose === true;
|
||||
// ugh TODO: Fix this
|
||||
const proxyConfig = (global as any).PROXY_CONFIG;
|
||||
const useProxy = !!proxyConfig && !options.noProxy;
|
||||
const port = options.port != null ? parseInt(options.port, 10) : undefined;
|
||||
|
||||
// if we're doing a direct SSH connection locally...
|
||||
if (
|
||||
validateDotLocalUrl(applicationOrDevice) ||
|
||||
validateIPAddress(applicationOrDevice)
|
||||
) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: applicationOrDevice,
|
||||
port,
|
||||
verbose,
|
||||
service: params.serviceName,
|
||||
});
|
||||
}
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
await exitIfNotLoggedIn();
|
||||
const uuid = await getOnlineTargetUuid(sdk, applicationOrDevice);
|
||||
let version: string | undefined;
|
||||
let id: number | undefined;
|
||||
|
||||
try {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
id = device.id;
|
||||
version = device.supervisor_version;
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaDeviceNotFound) {
|
||||
exitWithExpectedError(`Could not find device: ${uuid}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [hasTunnelBin, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? await hasbin('proxytunnel') : undefined,
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('proxyUrl'),
|
||||
]);
|
||||
|
||||
const getSshProxyCommand = () => {
|
||||
if (!useProxy) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!hasTunnelBin) {
|
||||
console.warn(stripIndent`
|
||||
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
|
||||
Please install it if you want to route the \`balena ssh\` requests through the proxy.
|
||||
Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config
|
||||
for the \`ssh\` requests.
|
||||
|
||||
Attempting the unproxied request for now.`);
|
||||
return '';
|
||||
}
|
||||
|
||||
let tunnelOptions: Dictionary<string> = {
|
||||
proxy: `${proxyConfig.host}:${proxyConfig.port}`,
|
||||
dest: '%h:%p',
|
||||
};
|
||||
const { proxyAuth } = proxyConfig;
|
||||
if (proxyAuth) {
|
||||
const i = proxyAuth.indexOf(':');
|
||||
tunnelOptions = {
|
||||
user: proxyAuth.substring(0, i),
|
||||
pass: proxyAuth.substring(i + 1),
|
||||
...tunnelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const ProxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
|
||||
return `-o ${bash.args({ ProxyCommand }, '', '=')}`;
|
||||
};
|
||||
|
||||
const proxyCommand = getSshProxyCommand();
|
||||
|
||||
if (username == null) {
|
||||
exitWithExpectedError(
|
||||
`Opening an SSH connection to a remote device requires you to be logged in.`,
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, we have a long uuid with a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.serviceName != null) {
|
||||
containerId = await getContainerId(
|
||||
sdk,
|
||||
uuid,
|
||||
params.serviceName,
|
||||
{
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
},
|
||||
version,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
if (containerId != null) {
|
||||
accessCommand = `enter ${uuid} ${containerId}`;
|
||||
} else {
|
||||
accessCommand = `host ${uuid}`;
|
||||
}
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
command: accessCommand,
|
||||
verbose,
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
await child_process.spawn(subShellCommand.program, subShellCommand.args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
},
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports = require('resin-sync').capitano('resin-cli')
|
303
lib/actions/tags.ts
Normal file
303
lib/actions/tags.ts
Normal file
@ -0,0 +1,303 @@
|
||||
/*
|
||||
Copyright 2016-2018 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 { ApplicationTag, DeviceTag, ReleaseTag } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import {
|
||||
disambiguateReleaseParam,
|
||||
normalizeUuidProp,
|
||||
} from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
export const list: CommandDefinition<
|
||||
{},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: number | string;
|
||||
release_raw?: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'tags',
|
||||
description: 'list all resource tags',
|
||||
help: stripIndent`
|
||||
Use this command to list all tags for
|
||||
a particular application, device or release.
|
||||
|
||||
This command lists all application/device/release tags.
|
||||
|
||||
Example:
|
||||
|
||||
$ balena tags --application MyApp
|
||||
$ balena tags --device 7cf02a6
|
||||
$ balena tags --release 1234
|
||||
$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
commandOptions.optionalRelease,
|
||||
],
|
||||
permission: 'user',
|
||||
async action(_params, options, done) {
|
||||
normalizeUuidProp(options, 'device');
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
|
||||
return Bluebird.try<ApplicationTag[] | DeviceTag[] | ReleaseTag[]>(
|
||||
async () => {
|
||||
const wrongParametersError = stripIndent`
|
||||
To list resource tags, 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
|
||||
`;
|
||||
|
||||
if (
|
||||
_.filter([options.application, options.device, options.release])
|
||||
.length !== 1
|
||||
) {
|
||||
return exitWithExpectedError(wrongParametersError);
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.getAllByApplication(
|
||||
options.application,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.getAllByDevice(options.device);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
options.release_raw,
|
||||
);
|
||||
return balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
// return never, so that TS typings are happy
|
||||
return exitWithExpectedError(wrongParametersError);
|
||||
},
|
||||
)
|
||||
.tap(function(environmentVariables) {
|
||||
if (_.isEmpty(environmentVariables)) {
|
||||
exitWithExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(
|
||||
visuals.table.horizontal(environmentVariables, [
|
||||
'id',
|
||||
'tag_key',
|
||||
'value',
|
||||
]),
|
||||
);
|
||||
})
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const set: CommandDefinition<
|
||||
{
|
||||
tagKey: string;
|
||||
value?: string;
|
||||
},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: number | string;
|
||||
release_raw: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'tag set <tagKey> [value]',
|
||||
description: 'set a resource tag',
|
||||
help: stripIndent`
|
||||
Use this command to set a tag to 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. When the value isn't
|
||||
provided, a tag with an empty value is created.
|
||||
|
||||
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
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
commandOptions.optionalRelease,
|
||||
],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(options, 'device');
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
|
||||
return Bluebird.try(async () => {
|
||||
if (_.isEmpty(params.tagKey)) {
|
||||
return exitWithExpectedError('No tag key was provided');
|
||||
}
|
||||
|
||||
if (
|
||||
_.filter([options.application, options.device, options.release])
|
||||
.length !== 1
|
||||
) {
|
||||
return exitWithExpectedError(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
|
||||
`);
|
||||
}
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = '';
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.set(
|
||||
options.application,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.set(
|
||||
options.device,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
options.release_raw,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.set(
|
||||
releaseParam,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const remove: CommandDefinition<
|
||||
{
|
||||
tagKey: string;
|
||||
},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: number | string;
|
||||
release_raw?: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'tag rm <tagKey>',
|
||||
description: 'remove a resource tag',
|
||||
help: stripIndent`
|
||||
Use this command to remove a tag from an application, device or release.
|
||||
|
||||
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
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
commandOptions.optionalRelease,
|
||||
],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
|
||||
return Bluebird.try(async () => {
|
||||
if (_.isEmpty(params.tagKey)) {
|
||||
return exitWithExpectedError('No tag key was provided');
|
||||
}
|
||||
|
||||
if (
|
||||
_.filter([options.application, options.device, options.release])
|
||||
.length !== 1
|
||||
) {
|
||||
return exitWithExpectedError(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
|
||||
`);
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.remove(
|
||||
options.application,
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.remove(options.device, params.tagKey);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
options.release_raw,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.remove(releaseParam, params.tagKey);
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
235
lib/actions/tunnel.ts
Normal file
235
lib/actions/tunnel.ts
Normal file
@ -0,0 +1,235 @@
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { createServer, Server, Socket } from 'net';
|
||||
import { isArray } from 'util';
|
||||
|
||||
import { getOnlineTargetUuid } from '../utils/patterns';
|
||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
||||
|
||||
interface Args {
|
||||
deviceOrApplication: string;
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
deviceOrApplication_raw: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
port: string | string[];
|
||||
}
|
||||
|
||||
class InvalidPortMappingError extends Error {
|
||||
constructor(mapping: string) {
|
||||
super(`'${mapping}' is not a valid port mapping.`);
|
||||
}
|
||||
}
|
||||
|
||||
class NoPortsDefinedError extends Error {
|
||||
constructor() {
|
||||
super('No ports have been provided.');
|
||||
}
|
||||
}
|
||||
|
||||
const isValidPort = (port: number) => {
|
||||
const MAX_PORT_VALUE = Math.pow(2, 16) - 1;
|
||||
return port > 0 && port <= MAX_PORT_VALUE;
|
||||
};
|
||||
|
||||
export const tunnel: CommandDefinition<Args, Options> = {
|
||||
signature: 'tunnel <deviceOrApplication>',
|
||||
description: 'Tunnel local ports to your balenaOS device',
|
||||
help: stripIndent`
|
||||
Use this command to open local ports which tunnel to listening ports on your balenaOS device.
|
||||
|
||||
For example, you could open port 8080 on your local machine to connect to your managed balenaOS
|
||||
device running a web server listening on port 3000.
|
||||
|
||||
You can tunnel multiple ports at any given time.
|
||||
|
||||
Examples:
|
||||
|
||||
# map remote port 22222 to localhost:22222
|
||||
$ balena tunnel abcde12345 -p 22222
|
||||
|
||||
# map remote port 22222 to localhost:222
|
||||
$ balena tunnel abcde12345 -p 22222:222
|
||||
|
||||
# map remote port 22222 to any address on your host machine, port 22222
|
||||
$ balena tunnel abcde12345 -p 22222:0.0.0.0
|
||||
|
||||
# map remote port 22222 to any address on your host machine, port 222
|
||||
$ balena tunnel abcde12345 -p 22222:0.0.0.0:222
|
||||
|
||||
# multiple port tunnels can be specified at any one time
|
||||
$ balena tunnel abcde12345 -p 8080:3000 -p 8081:9000
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'port',
|
||||
parameter: 'port',
|
||||
alias: 'p',
|
||||
description: 'The mapping of remote to local ports.',
|
||||
},
|
||||
],
|
||||
|
||||
primary: true,
|
||||
|
||||
action: async (params, options) => {
|
||||
const deviceOrApplication =
|
||||
params.deviceOrApplication_raw || params.deviceOrApplication;
|
||||
const Logger = await import('../utils/logger');
|
||||
const logger = Logger.getLogger();
|
||||
const balena = await import('balena-sdk');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
|
||||
const logConnection = (
|
||||
fromHost: string,
|
||||
fromPort: number,
|
||||
localAddress: string,
|
||||
localPort: number,
|
||||
deviceAddress: string,
|
||||
devicePort: number,
|
||||
err?: Error,
|
||||
) => {
|
||||
const logMessage = `${fromHost}:${fromPort} => ${localAddress}:${localPort} ===> ${deviceAddress}:${devicePort}`;
|
||||
|
||||
if (err) {
|
||||
logger.logError(`${logMessage} :: ${err.message}`);
|
||||
} else {
|
||||
logger.logLogs(logMessage);
|
||||
}
|
||||
};
|
||||
|
||||
if (options.port === undefined) {
|
||||
throw new NoPortsDefinedError();
|
||||
}
|
||||
|
||||
const ports =
|
||||
typeof options.port !== 'string' && isArray(options.port)
|
||||
? (options.port as string[])
|
||||
: [options.port as string];
|
||||
|
||||
const uuid = await getOnlineTargetUuid(sdk, deviceOrApplication);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
const localListeners = _.chain(ports)
|
||||
.map(mapping => {
|
||||
const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec(
|
||||
mapping,
|
||||
);
|
||||
|
||||
if (regexResult === null) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// grab the groups
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let [, remotePort, localAddress, localPort] = regexResult;
|
||||
|
||||
if (
|
||||
!isValidPort(parseInt(localPort, undefined)) ||
|
||||
!isValidPort(parseInt(remotePort, undefined))
|
||||
) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// default bind to localAddress
|
||||
if (localAddress == null) {
|
||||
localAddress = 'localhost';
|
||||
}
|
||||
|
||||
// default use same port number locally as remote
|
||||
if (localPort == null) {
|
||||
localPort = remotePort;
|
||||
}
|
||||
|
||||
return {
|
||||
localPort: parseInt(localPort, undefined),
|
||||
localAddress,
|
||||
remotePort: parseInt(remotePort, undefined),
|
||||
};
|
||||
})
|
||||
.map(({ localPort, localAddress, remotePort }) => {
|
||||
return tunnelConnectionToDevice(device.uuid, remotePort, sdk)
|
||||
.then(handler =>
|
||||
createServer((client: Socket) => {
|
||||
return handler(client)
|
||||
.then(() => {
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
})
|
||||
.catch(err =>
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.then(
|
||||
server =>
|
||||
new Bluebird.Promise<Server>((resolve, reject) => {
|
||||
server.on('error', reject);
|
||||
server.listen(localPort, localAddress, () => {
|
||||
resolve(server);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.value();
|
||||
|
||||
const results = await Promise.all(localListeners);
|
||||
if (!results.includes(true)) {
|
||||
throw new Error('No ports are valid for tunnelling');
|
||||
}
|
||||
|
||||
logger.logInfo('Waiting for connections...');
|
||||
},
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
exports.availableDrives =
|
||||
# TODO: dedupe with https://github.com/resin-io-modules/resin-cli-visuals/blob/master/lib/widgets/drive/index.coffee
|
||||
signature: 'util available-drives'
|
||||
description: 'list available drives'
|
||||
help: """
|
||||
Use this command to list your machine's drives usable for writing the OS image to.
|
||||
Skips the system drives.
|
||||
"""
|
||||
action: ->
|
||||
Promise = require('bluebird')
|
||||
drivelist = require('drivelist')
|
||||
driveListAsync = Promise.promisify(drivelist.list)
|
||||
chalk = require('chalk')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
formatDrive = (drive) ->
|
||||
size = drive.size / 1000000000
|
||||
return {
|
||||
device: drive.device
|
||||
size: "#{size.toFixed(1)} GB"
|
||||
description: drive.description
|
||||
}
|
||||
|
||||
getDrives = ->
|
||||
driveListAsync().then (drives) ->
|
||||
return _.reject(drives, system: true)
|
||||
|
||||
getDrives()
|
||||
.then (drives) ->
|
||||
if not drives.length
|
||||
console.error("#{chalk.red('x')} No available drives were detected, plug one in!")
|
||||
return
|
||||
|
||||
console.log visuals.table.horizontal drives.map(formatDrive), [
|
||||
'device'
|
||||
'size'
|
||||
'description'
|
||||
]
|
65
lib/actions/util.ts
Normal file
65
lib/actions/util.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
/*
|
||||
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';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const availableDrives: CommandDefinition<{}, {}> = {
|
||||
signature: 'util available-drives',
|
||||
description: 'list available drives',
|
||||
help: stripIndent`
|
||||
Use this command to list your machine's drives usable for writing the OS image to.
|
||||
Skips the system drives.
|
||||
`,
|
||||
async action() {
|
||||
const sdk = await import('etcher-sdk');
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
|
||||
function formatDrive(drive: any) {
|
||||
const size = drive.size / 1000000000;
|
||||
return {
|
||||
device: drive.device,
|
||||
size: `${size.toFixed(1)} GB`,
|
||||
description: drive.description,
|
||||
};
|
||||
}
|
||||
|
||||
if (scanner.drives.size === 0) {
|
||||
console.error(
|
||||
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
visuals.table.horizontal(Array.from(scanner.drives).map(formatDrive), [
|
||||
'device',
|
||||
'size',
|
||||
'description',
|
||||
]),
|
||||
);
|
||||
}
|
||||
scanner.stop();
|
||||
},
|
||||
};
|
@ -1,75 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.wizard =
|
||||
signature: 'quickstart [name]'
|
||||
description: 'getting started with resin.io'
|
||||
help: '''
|
||||
Use this command to run a friendly wizard to get started with resin.io.
|
||||
|
||||
The wizard will guide you through:
|
||||
|
||||
- Create an application.
|
||||
- Initialise an SDCard with the resin.io operating system.
|
||||
- Associate an existing project directory with your resin.io application.
|
||||
- Push your project to your devices.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin quickstart
|
||||
$ resin quickstart MyApp
|
||||
'''
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
console.info('Looks like you\'re not logged in yet!')
|
||||
console.info('Lets go through a quick wizard to get you started.\n')
|
||||
return capitanoRunAsync('login')
|
||||
.then ->
|
||||
return if params.name?
|
||||
patterns.selectOrCreateApplication().tap (applicationName) ->
|
||||
resin.models.application.has(applicationName).then (hasApplication) ->
|
||||
return applicationName if hasApplication
|
||||
capitanoRunAsync("app create #{applicationName}")
|
||||
.then (applicationName) ->
|
||||
params.name = applicationName
|
||||
.then ->
|
||||
return capitanoRunAsync("device init --application #{params.name}")
|
||||
.tap(patterns.awaitDevice)
|
||||
.then (uuid) ->
|
||||
return capitanoRunAsync("device #{uuid}")
|
||||
.then ->
|
||||
return resin.models.application.get(params.name)
|
||||
.then (application) ->
|
||||
console.log """
|
||||
Your device is ready to start pushing some code!
|
||||
|
||||
Check our official documentation for more information:
|
||||
|
||||
http://docs.resin.io/#/pages/introduction/introduction.md
|
||||
|
||||
Clone an example or go to an existing application directory and run:
|
||||
|
||||
$ git remote add resin #{application.git_repository}
|
||||
$ git push resin master
|
||||
"""
|
||||
.nodeify(done)
|
165
lib/app-capitano.coffee
Normal file
165
lib/app-capitano.coffee
Normal file
@ -0,0 +1,165 @@
|
||||
###
|
||||
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.
|
||||
###
|
||||
|
||||
Promise = require('bluebird')
|
||||
capitano = require('capitano')
|
||||
actions = require('./actions')
|
||||
events = require('./events')
|
||||
|
||||
capitano.permission 'user', (done) ->
|
||||
require('./utils/patterns').exitIfNotLoggedIn()
|
||||
.then(done, done)
|
||||
|
||||
capitano.command
|
||||
signature: '*'
|
||||
action: (params, options, done) ->
|
||||
capitano.execute(command: 'help', done)
|
||||
process.exitCode = process.exitCode || 1
|
||||
|
||||
capitano.globalOption
|
||||
signature: 'help'
|
||||
boolean: true
|
||||
alias: 'h'
|
||||
|
||||
capitano.globalOption
|
||||
signature: 'version'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
|
||||
# ---------- Help Module ----------
|
||||
capitano.command(actions.help.help)
|
||||
|
||||
# ---------- Api key module ----------
|
||||
capitano.command(actions.apiKey.generate)
|
||||
|
||||
# ---------- App Module ----------
|
||||
capitano.command(actions.app.create)
|
||||
capitano.command(actions.app.list)
|
||||
capitano.command(actions.app.remove)
|
||||
capitano.command(actions.app.restart)
|
||||
capitano.command(actions.app.info)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.whoami)
|
||||
|
||||
# ---------- Device Module ----------
|
||||
capitano.command(actions.device.list)
|
||||
capitano.command(actions.device.supported)
|
||||
capitano.command(actions.device.rename)
|
||||
capitano.command(actions.device.init)
|
||||
capitano.command(actions.device.remove)
|
||||
capitano.command(actions.device.identify)
|
||||
capitano.command(actions.device.reboot)
|
||||
capitano.command(actions.device.shutdown)
|
||||
capitano.command(actions.device.enableDeviceUrl)
|
||||
capitano.command(actions.device.disableDeviceUrl)
|
||||
capitano.command(actions.device.getDeviceUrl)
|
||||
capitano.command(actions.device.hasDeviceUrl)
|
||||
capitano.command(actions.device.register)
|
||||
capitano.command(actions.device.move)
|
||||
capitano.command(actions.device.osUpdate)
|
||||
capitano.command(actions.device.info)
|
||||
|
||||
# ---------- Notes Module ----------
|
||||
capitano.command(actions.notes.set)
|
||||
|
||||
# ---------- Keys Module ----------
|
||||
capitano.command(actions.keys.list)
|
||||
capitano.command(actions.keys.add)
|
||||
capitano.command(actions.keys.info)
|
||||
capitano.command(actions.keys.remove)
|
||||
|
||||
# ---------- Tags Module ----------
|
||||
capitano.command(actions.tags.list)
|
||||
capitano.command(actions.tags.set)
|
||||
capitano.command(actions.tags.remove)
|
||||
|
||||
# ---------- OS Module ----------
|
||||
capitano.command(actions.os.versions)
|
||||
capitano.command(actions.os.download)
|
||||
capitano.command(actions.os.buildConfig)
|
||||
capitano.command(actions.os.initialize)
|
||||
|
||||
# ---------- Config Module ----------
|
||||
capitano.command(actions.config.read)
|
||||
capitano.command(actions.config.write)
|
||||
capitano.command(actions.config.inject)
|
||||
capitano.command(actions.config.reconfigure)
|
||||
capitano.command(actions.config.generate)
|
||||
|
||||
# ---------- Settings Module ----------
|
||||
capitano.command(actions.settings.list)
|
||||
|
||||
# ---------- Logs Module ----------
|
||||
capitano.command(actions.logs.logs)
|
||||
|
||||
# ---------- Tunnel Module ----------
|
||||
capitano.command(actions.tunnel.tunnel)
|
||||
|
||||
# ---------- Preload Module ----------
|
||||
capitano.command(actions.preload)
|
||||
|
||||
# ---------- SSH Module ----------
|
||||
capitano.command(actions.ssh.ssh)
|
||||
|
||||
# ---------- Local balenaOS Module ----------
|
||||
capitano.command(actions.local.configure)
|
||||
capitano.command(actions.local.flash)
|
||||
capitano.command(actions.scan)
|
||||
|
||||
# ---------- Public utils ----------
|
||||
capitano.command(actions.util.availableDrives)
|
||||
|
||||
# ---------- Internal utils ----------
|
||||
capitano.command(actions.internal.osInit)
|
||||
capitano.command(actions.internal.scanDevices)
|
||||
|
||||
#------------ Local build and deploy -------
|
||||
capitano.command(actions.build)
|
||||
capitano.command(actions.deploy)
|
||||
|
||||
#------------ Push/remote builds -------
|
||||
capitano.command(actions.push.push)
|
||||
|
||||
#------------ Join/Leave -------
|
||||
capitano.command(actions.join.join)
|
||||
capitano.command(actions.leave.leave)
|
||||
|
||||
exports.run = (argv) ->
|
||||
cli = capitano.parse(argv.slice(2))
|
||||
runCommand = ->
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
trackCommand = ->
|
||||
getMatchCommandAsync = Promise.promisify(capitano.state.getMatchCommand)
|
||||
getMatchCommandAsync(cli.command)
|
||||
.then (command) ->
|
||||
# cmdSignature is literally a string like, for example:
|
||||
# "push <applicationOrDevice>"
|
||||
# ("applicationOrDevice" is NOT replaced with its actual value)
|
||||
# In case of failures like an inexistent or invalid command,
|
||||
# command.signature.toString() returns '*'
|
||||
cmdSignature = command.signature.toString()
|
||||
events.trackCommand(cmdSignature)
|
||||
|
||||
Promise.all([trackCommand(), runCommand()])
|
||||
.catch(require('./errors').handleError)
|
18
lib/app-capitano.d.ts
vendored
Normal file
18
lib/app-capitano.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export async function run(argv: string[]);
|
124
lib/app-common.ts
Normal file
124
lib/app-common.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/clients/node/
|
||||
*/
|
||||
function setupRaven() {
|
||||
const Raven = require('raven');
|
||||
Raven.disableConsoleAlerts();
|
||||
Raven.config(require('./config').sentryDsn, {
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: true,
|
||||
release: require('../package.json').version,
|
||||
}).install(function(_logged: any, error: Error) {
|
||||
console.error(error);
|
||||
return process.exit(1);
|
||||
});
|
||||
|
||||
Raven.setContext({
|
||||
extra: {
|
||||
args: process.argv,
|
||||
node_version: process.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function checkNodeVersion() {
|
||||
const validNodeVersions = require('../package.json').engines.node;
|
||||
if (!require('semver').satisfies(process.version, validNodeVersions)) {
|
||||
const { stripIndent } = require('common-tags');
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${
|
||||
process.version
|
||||
}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behaviour. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalHttpProxy() {
|
||||
// Doing this before requiring any other modules,
|
||||
// including the 'balena-sdk', to prevent any module from reading the http proxy config
|
||||
// before us
|
||||
const globalTunnel = require('global-tunnel-ng');
|
||||
const settings = require('balena-settings-client');
|
||||
let proxy;
|
||||
try {
|
||||
proxy = settings.get('proxy') || null;
|
||||
} catch (error1) {
|
||||
proxy = null;
|
||||
}
|
||||
|
||||
// Init the tunnel even if the proxy is not configured
|
||||
// because it can also get the proxy from the http(s)_proxy env var
|
||||
// If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy);
|
||||
|
||||
// TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
|
||||
(global as any).PROXY_CONFIG = globalTunnel.proxyConfig;
|
||||
}
|
||||
|
||||
function setupBalenaSdkSharedOptions() {
|
||||
// We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
// options correctly so we can do safely in submodules
|
||||
const BalenaSdk = require('balena-sdk');
|
||||
const settings = require('balena-settings-client');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get('apiUrl'),
|
||||
imageMakerUrl: settings.get('imageMakerUrl'),
|
||||
dataDirectory: settings.get('dataDirectory'),
|
||||
retries: 2,
|
||||
});
|
||||
}
|
||||
|
||||
let BluebirdConfigured = false;
|
||||
|
||||
/**
|
||||
* Configure Bluebird and assign it as the global promise library.
|
||||
* Modules like `stream-to-promise` will otherwise produce native promises,
|
||||
* which leads to errors as much of the CLI CoffeeScript code expects Bluebird
|
||||
* promises.
|
||||
*/
|
||||
export function configureBluebird() {
|
||||
if (BluebirdConfigured) {
|
||||
return;
|
||||
}
|
||||
BluebirdConfigured = true;
|
||||
const Bluebird = require('bluebird');
|
||||
Bluebird.config({
|
||||
longStackTraces: process.env.DEBUG ? true : false,
|
||||
});
|
||||
if (!(global as any)['@@any-promise/REGISTRATION']) {
|
||||
require('any-promise/register/bluebird');
|
||||
}
|
||||
}
|
||||
|
||||
export function globalInit() {
|
||||
setupRaven();
|
||||
checkNodeVersion();
|
||||
configureBluebird();
|
||||
setupGlobalHttpProxy();
|
||||
setupBalenaSdkSharedOptions();
|
||||
|
||||
// check for CLI updates once a day
|
||||
require('./utils/update').notify();
|
||||
}
|
60
lib/app-oclif.ts
Normal file
60
lib/app-oclif.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Main } from '@oclif/command';
|
||||
import { ExitError } from '@oclif/errors';
|
||||
|
||||
import { trackPromise } from './hooks/prerun/track';
|
||||
|
||||
class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AppOptions = import('./preparser').AppOptions;
|
||||
|
||||
/**
|
||||
* oclif CLI entrypoint
|
||||
*/
|
||||
export async function run(command: string[], options: AppOptions) {
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
}
|
||||
},
|
||||
(error: Error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
if (error instanceof ExitError && error.oclif.exit === 0) {
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
}
|
||||
}
|
224
lib/app.coffee
224
lib/app.coffee
@ -1,224 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
Raven = require('raven')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config require('./config').sentryDsn,
|
||||
captureUnhandledRejections: true
|
||||
release: require('../package.json').version
|
||||
.install (logged, error) ->
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
Raven.setContext
|
||||
extra:
|
||||
args: process.argv
|
||||
node_version: process.version
|
||||
|
||||
validNodeVersions = require('../package.json').engines.node
|
||||
if not require('semver').satisfies(process.version, validNodeVersions)
|
||||
console.warn """
|
||||
Warning: this version of Node does not match the requirements of this package.
|
||||
This package expects #{validNodeVersions}, but you're using #{process.version}.
|
||||
This may cause unexpected behaviour.
|
||||
|
||||
To upgrade your Node, visit https://nodejs.org/en/download/
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Doing this before requiring any other modules,
|
||||
# including the 'resin-sdk', to prevent any module from reading the http proxy config
|
||||
# before us
|
||||
globalTunnel = require('global-tunnel-ng')
|
||||
settings = require('resin-settings-client')
|
||||
try
|
||||
proxy = settings.get('proxy') or null
|
||||
catch
|
||||
proxy = null
|
||||
# Init the tunnel even if the proxy is not configured
|
||||
# because it can also get the proxy from the http(s)_proxy env var
|
||||
# If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy)
|
||||
|
||||
# TODO: make this a feature of capitano https://github.com/resin-io/capitano/issues/48
|
||||
global.PROXY_CONFIG = globalTunnel.proxyConfig
|
||||
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
capitano = require('capitano')
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
|
||||
# We don't yet use resin-sdk directly everywhere, but we set up shared
|
||||
# options correctly so we can do safely in submodules
|
||||
require('resin-sdk').setSharedOptions(
|
||||
apiUrl: settings.get('apiUrl')
|
||||
imageMakerUrl: settings.get('imageMakerUrl')
|
||||
dataDirectory: settings.get('dataDirectory')
|
||||
retries: 2
|
||||
)
|
||||
# Keep using sdk-preconfigured for now, but only temporarily
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
actions = require('./actions')
|
||||
errors = require('./errors')
|
||||
events = require('./events')
|
||||
plugins = require('./utils/plugins')
|
||||
update = require('./utils/update')
|
||||
|
||||
# Assign bluebird as the global promise library
|
||||
# stream-to-promise will produce native promises if not
|
||||
# for this module, which could wreak havoc in this
|
||||
# bluebird-only codebase.
|
||||
require('any-promise/register/bluebird')
|
||||
|
||||
capitano.permission 'user', (done) ->
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
if not isLoggedIn
|
||||
throw new Error '''
|
||||
You have to log in to continue
|
||||
|
||||
Run the following command to go through the login wizard:
|
||||
|
||||
$ resin login
|
||||
'''
|
||||
.nodeify(done)
|
||||
|
||||
capitano.command
|
||||
signature: '*'
|
||||
action: ->
|
||||
capitano.execute(command: 'help')
|
||||
|
||||
capitano.globalOption
|
||||
signature: 'help'
|
||||
boolean: true
|
||||
alias: 'h'
|
||||
|
||||
# ---------- Info Module ----------
|
||||
capitano.command(actions.info.version)
|
||||
|
||||
# ---------- Help Module ----------
|
||||
capitano.command(actions.help.help)
|
||||
|
||||
# ---------- Wizard Module ----------
|
||||
capitano.command(actions.wizard.wizard)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.signup)
|
||||
capitano.command(actions.auth.whoami)
|
||||
|
||||
# ---------- App Module ----------
|
||||
capitano.command(actions.app.create)
|
||||
capitano.command(actions.app.list)
|
||||
capitano.command(actions.app.remove)
|
||||
capitano.command(actions.app.restart)
|
||||
capitano.command(actions.app.info)
|
||||
|
||||
# ---------- Device Module ----------
|
||||
capitano.command(actions.device.list)
|
||||
capitano.command(actions.device.supported)
|
||||
capitano.command(actions.device.rename)
|
||||
capitano.command(actions.device.init)
|
||||
capitano.command(actions.device.remove)
|
||||
capitano.command(actions.device.identify)
|
||||
capitano.command(actions.device.reboot)
|
||||
capitano.command(actions.device.shutdown)
|
||||
capitano.command(actions.device.enableDeviceUrl)
|
||||
capitano.command(actions.device.disableDeviceUrl)
|
||||
capitano.command(actions.device.getDeviceUrl)
|
||||
capitano.command(actions.device.hasDeviceUrl)
|
||||
capitano.command(actions.device.register)
|
||||
capitano.command(actions.device.move)
|
||||
capitano.command(actions.device.info)
|
||||
|
||||
# ---------- Notes Module ----------
|
||||
capitano.command(actions.notes.set)
|
||||
|
||||
# ---------- Keys Module ----------
|
||||
capitano.command(actions.keys.list)
|
||||
capitano.command(actions.keys.add)
|
||||
capitano.command(actions.keys.info)
|
||||
capitano.command(actions.keys.remove)
|
||||
|
||||
# ---------- Env Module ----------
|
||||
capitano.command(actions.env.list)
|
||||
capitano.command(actions.env.add)
|
||||
capitano.command(actions.env.rename)
|
||||
capitano.command(actions.env.remove)
|
||||
|
||||
# ---------- OS Module ----------
|
||||
capitano.command(actions.os.versions)
|
||||
capitano.command(actions.os.download)
|
||||
capitano.command(actions.os.buildConfig)
|
||||
capitano.command(actions.os.configure)
|
||||
capitano.command(actions.os.initialize)
|
||||
|
||||
# ---------- Config Module ----------
|
||||
capitano.command(actions.config.read)
|
||||
capitano.command(actions.config.write)
|
||||
capitano.command(actions.config.inject)
|
||||
capitano.command(actions.config.reconfigure)
|
||||
capitano.command(actions.config.generate)
|
||||
|
||||
# ---------- Settings Module ----------
|
||||
capitano.command(actions.settings.list)
|
||||
|
||||
# ---------- Logs Module ----------
|
||||
capitano.command(actions.logs)
|
||||
|
||||
# ---------- Sync Module ----------
|
||||
capitano.command(actions.sync)
|
||||
|
||||
# ---------- Preload Module ----------
|
||||
capitano.command(actions.preload)
|
||||
|
||||
# ---------- SSH Module ----------
|
||||
capitano.command(actions.ssh)
|
||||
|
||||
# ---------- Local ResinOS Module ----------
|
||||
capitano.command(actions.local.configure)
|
||||
capitano.command(actions.local.flash)
|
||||
capitano.command(actions.local.logs)
|
||||
capitano.command(actions.local.push)
|
||||
capitano.command(actions.local.ssh)
|
||||
capitano.command(actions.local.scan)
|
||||
capitano.command(actions.local.stop)
|
||||
|
||||
# ---------- Public utils ----------
|
||||
capitano.command(actions.util.availableDrives)
|
||||
|
||||
# ---------- Internal utils ----------
|
||||
capitano.command(actions.internal.osInit)
|
||||
|
||||
#------------ Local build and deploy -------
|
||||
capitano.command(actions.build)
|
||||
capitano.command(actions.deploy)
|
||||
|
||||
update.notify()
|
||||
|
||||
plugins.register(/^resin-plugin-(.+)$/).then ->
|
||||
cli = capitano.parse(process.argv)
|
||||
|
||||
runCommand = ->
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
|
||||
.catch(errors.handle)
|
77
lib/app.ts
Normal file
77
lib/app.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
|
||||
* call this function.
|
||||
*/
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
// for use of the standalone zip package. See pkgExec function.
|
||||
if (cliArgs.length > 3 && cliArgs[2] === 'pkgExec') {
|
||||
return pkgExec(cliArgs[3], cliArgs.slice(4));
|
||||
}
|
||||
|
||||
const { globalInit } = await import('./app-common');
|
||||
const { routeCliFramework } = await import('./preparser');
|
||||
|
||||
// globalInit() must be called very early on (before other imports) because
|
||||
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
|
||||
// shared options, and performs node version requirement checks.
|
||||
globalInit();
|
||||
await routeCliFramework(cliArgs, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the 'pkgExec' command, used as a way to provide a Node.js
|
||||
* interpreter for child_process.spawn()-like operations when the CLI is
|
||||
* executing as a standalone zip package (built-in Node interpreter) and
|
||||
* the system may not have a separate Node.js installation. A present use
|
||||
* case is a patched version of the 'windosu' package that requires a
|
||||
* Node.js interpreter to spawn a privileged child process.
|
||||
*
|
||||
* @param modFunc Path to a JS module that will be executed via require().
|
||||
* The modFunc argument may optionally contain a function name separated
|
||||
* by '::', for example '::main' in:
|
||||
* 'C:\\snapshot\\balena-cli\\node_modules\\windosu\\lib\\pipe.js::main'
|
||||
* in which case that function is executed in the require'd module.
|
||||
* @param args Optional arguments to passed through process.argv and as
|
||||
* arguments to the function specified via modFunc.
|
||||
*/
|
||||
async function pkgExec(modFunc: string, args: string[]) {
|
||||
const [modPath, funcName] = modFunc.split('::');
|
||||
let replacedModPath = modPath;
|
||||
const match = modPath
|
||||
.replace(/\\/g, '/')
|
||||
.match(/\/snapshot\/balena-cli\/(.+)/);
|
||||
if (match) {
|
||||
replacedModPath = `../${match[1]}`;
|
||||
}
|
||||
process.argv = [process.argv[0], process.argv[1], ...args];
|
||||
try {
|
||||
const mod: any = await import(replacedModPath);
|
||||
if (funcName) {
|
||||
await mod[funcName](...args);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error executing pkgExec "${modFunc}" [${args.join()}]`);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -18,19 +18,19 @@ limitations under the License.
|
||||
# @module auth
|
||||
###
|
||||
|
||||
open = require('open')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
open = require('opn')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
server = require('./server')
|
||||
utils = require('./utils')
|
||||
|
||||
###*
|
||||
# @summary Login to the Resin CLI using the web dashboard
|
||||
# @summary Login to the balena CLI using the web dashboard
|
||||
# @function
|
||||
# @public
|
||||
#
|
||||
# @description
|
||||
# This function opens the user's default browser and points it
|
||||
# to the Resin.io dashboard where the session token exchange will
|
||||
# to the balena dashboard where the session token exchange will
|
||||
# take place.
|
||||
#
|
||||
# Once the the token is retrieved, it's automatically persisted.
|
||||
@ -56,8 +56,8 @@ exports.login = ->
|
||||
# Leave a bit of time for the
|
||||
# server to get up and runing
|
||||
setTimeout ->
|
||||
open(loginUrl)
|
||||
open(loginUrl, { wait: false })
|
||||
, 1000
|
||||
|
||||
return server.awaitForToken(options)
|
||||
.tap(resin.auth.loginWithToken)
|
||||
.tap(balena.auth.loginWithToken)
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Resin CLI - Error</title>
|
||||
<title>Balena CLI - Error</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
@ -12,10 +12,10 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/sad.png" inline>
|
||||
<h1>Something went wrong</h1>
|
||||
<p>You couldn't login to the Resin CLI for some reason</p>
|
||||
<p>You couldn't login to the balena CLI for some reason</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://forums.resin.io/" class="button danger">Get help in our forums</a>
|
||||
<a href="https://forums.balena.io/" class="button danger">Get help in our forums</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Resin CLI - Success</title>
|
||||
<title>Balena CLI - Success</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/happy.png" inline>
|
||||
<h1>Success!</h1>
|
||||
<p>You successfully logged in the Resin CLI</p>
|
||||
<p>You successfully logged in the balena CLI</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="<%= dashboardUrl %>" class="button normal">Go to the dashboard</a>
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -18,7 +18,7 @@ express = require('express')
|
||||
path = require('path')
|
||||
bodyParser = require('body-parser')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
utils = require('./utils')
|
||||
|
||||
createServer = ({ port, isDev } = {}) ->
|
||||
@ -77,9 +77,9 @@ exports.awaitForToken = (options) ->
|
||||
Promise.try ->
|
||||
if not token
|
||||
throw new Error('No token')
|
||||
return utils.isTokenValid(token)
|
||||
.tap (isValid) ->
|
||||
if not isValid
|
||||
return utils.loginIfTokenValid(token)
|
||||
.tap (loggedIn) ->
|
||||
if not loggedIn
|
||||
throw new Error('Invalid token')
|
||||
.then ->
|
||||
renderAndDone({ request, response, viewName: 'success', token })
|
||||
@ -96,6 +96,6 @@ exports.awaitForToken = (options) ->
|
||||
exports.getContext = getContext = (viewName) ->
|
||||
if viewName is 'success'
|
||||
return Promise.props
|
||||
dashboardUrl: resin.settings.get('dashboardUrl')
|
||||
dashboardUrl: balena.settings.get('dashboardUrl')
|
||||
|
||||
return Promise.resolve({})
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
_ = require('lodash')
|
||||
url = require('url')
|
||||
Promise = require('bluebird')
|
||||
@ -38,11 +38,11 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
# characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25')
|
||||
|
||||
resin.settings.get('dashboardUrl').then (dashboardUrl) ->
|
||||
balena.settings.get('dashboardUrl').then (dashboardUrl) ->
|
||||
return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}")
|
||||
|
||||
###*
|
||||
# @summary Check if a token is valid
|
||||
# @summary Log in using a token, but only if the token is valid
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
@ -50,26 +50,31 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
# This function checks that the token is not only well-structured
|
||||
# but that it also authenticates with the server successfully.
|
||||
#
|
||||
# @param {String} sessionToken - token
|
||||
# @fulfil {Boolean} - whether is valid or not
|
||||
# If authenticated, the token is persisted, if not then the previous
|
||||
# login state is restored.
|
||||
#
|
||||
# @param {String} token - session token or api key
|
||||
# @fulfil {Boolean} - whether the login was successful or not
|
||||
# @returns {Promise}
|
||||
#
|
||||
# utils.isTokenValid('...').then (isValid) ->
|
||||
# if isValid
|
||||
# utils.loginIfTokenValid('...').then (loggedIn) ->
|
||||
# if loggedIn
|
||||
# console.log('Token is valid!')
|
||||
###
|
||||
exports.isTokenValid = (sessionToken) ->
|
||||
if not sessionToken? or _.isEmpty(sessionToken.trim())
|
||||
exports.loginIfTokenValid = (token) ->
|
||||
if not token? or _.isEmpty(token.trim())
|
||||
return Promise.resolve(false)
|
||||
|
||||
return resin.token.get().then (currentToken) ->
|
||||
resin.auth.loginWithToken(sessionToken)
|
||||
.return(sessionToken)
|
||||
.then(resin.auth.isLoggedIn)
|
||||
return balena.auth.getToken()
|
||||
.catchReturn(undefined)
|
||||
.then (currentToken) ->
|
||||
balena.auth.loginWithToken(token)
|
||||
.return(token)
|
||||
.then(balena.auth.isLoggedIn)
|
||||
.tap (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
|
||||
if currentToken?
|
||||
return resin.auth.loginWithToken(currentToken)
|
||||
return balena.auth.loginWithToken(currentToken)
|
||||
else
|
||||
return resin.auth.logout()
|
||||
return balena.auth.logout()
|
||||
|
@ -1 +1,21 @@
|
||||
exports.sentryDsn = 'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239'
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017-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.
|
||||
*/
|
||||
|
||||
export const BALENA_ENGINE_TMP_PATH = '/var/lib/docker/tmp';
|
||||
|
||||
export const sentryDsn =
|
||||
'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239';
|
||||
|
@ -1,38 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
chalk = require('chalk')
|
||||
errors = require('resin-cli-errors')
|
||||
patterns = require('./utils/patterns')
|
||||
Raven = require('raven')
|
||||
Promise = require('bluebird')
|
||||
|
||||
captureException = Promise.promisify(Raven.captureException.bind(Raven))
|
||||
|
||||
exports.handle = (error) ->
|
||||
message = errors.interpret(error)
|
||||
return if not message?
|
||||
|
||||
if process.env.DEBUG
|
||||
message = error.stack
|
||||
|
||||
patterns.printErrorMessage(message)
|
||||
|
||||
captureException(error)
|
||||
.timeout(1000)
|
||||
.catch(-> # Ignore any errors (from error logging, or timeouts)
|
||||
).finally ->
|
||||
process.exit(error.exitCode or 1)
|
137
lib/errors.ts
Normal file
137
lib/errors.ts
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as Raven from 'raven';
|
||||
|
||||
export class ExpectedError extends Error {}
|
||||
|
||||
export class NotLoggedInError extends ExpectedError {}
|
||||
|
||||
const captureException = Bluebird.promisify<string, Error>(
|
||||
Raven.captureException,
|
||||
{ context: Raven },
|
||||
);
|
||||
|
||||
function hasCode(error: any): error is Error & { code: string } {
|
||||
return error.code != null;
|
||||
}
|
||||
|
||||
function treatFailedBindingAsMissingModule(error: any): void {
|
||||
if (error.message.startsWith('Could not locate the bindings file.')) {
|
||||
error.code = 'MODULE_NOT_FOUND';
|
||||
}
|
||||
}
|
||||
|
||||
function interpret(error: Error): string {
|
||||
treatFailedBindingAsMissingModule(error);
|
||||
|
||||
if (hasCode(error)) {
|
||||
const errorCodeHandler = messages[error.code];
|
||||
const message = errorCodeHandler && errorCodeHandler(error);
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(error.message)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
return error.message;
|
||||
}
|
||||
|
||||
const messages: {
|
||||
[key: string]: (error: Error & { path?: string }) => string;
|
||||
} = {
|
||||
EISDIR: error => `File is a directory: ${error.path}`,
|
||||
|
||||
ENOENT: error => `No such file or directory: ${error.path}`,
|
||||
|
||||
ENOGIT: () => stripIndent`
|
||||
Git is not installed on this system.
|
||||
Head over to http://git-scm.com to install it and run this command again.`,
|
||||
|
||||
EPERM: () => stripIndent`
|
||||
You don't have sufficient privileges to run this operation.
|
||||
${
|
||||
os.platform() === 'win32'
|
||||
? 'Run a new Command Prompt as administrator and try running this command again.'
|
||||
: 'Try running this command again prefixing it with `sudo`.'
|
||||
}
|
||||
|
||||
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
|
||||
|
||||
EACCES: e => messages.EPERM(e),
|
||||
|
||||
ETIMEDOUT: () =>
|
||||
'Oops something went wrong, please check your connection and try again.',
|
||||
|
||||
MODULE_NOT_FOUND: () => stripIndent`
|
||||
Part of the CLI could not be loaded. This typically means your CLI install is in a broken state.
|
||||
${
|
||||
os.arch() === 'x64'
|
||||
? 'You can normally fix this by uninstalling and reinstalling the CLI.'
|
||||
: stripIndent`
|
||||
You're using an unsupported architecture (${os.arch()}), so this is typically caused by missing native modules.
|
||||
Reinstalling may help, but pay attention to errors in native module build steps en route.
|
||||
`
|
||||
}
|
||||
`,
|
||||
|
||||
BalenaExpiredToken: () => stripIndent`
|
||||
Looks like your session token is expired.
|
||||
Please try logging in again with:
|
||||
$ balena login`,
|
||||
};
|
||||
|
||||
export async function handleError(error: any) {
|
||||
const { printErrorMessage } = await import('./utils/patterns');
|
||||
|
||||
process.exitCode =
|
||||
error.exitCode === 0
|
||||
? 0
|
||||
: parseInt(error.exitCode, 10) || process.exitCode || 1;
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
printErrorMessage(String(error));
|
||||
return;
|
||||
}
|
||||
|
||||
const message = [interpret(error)];
|
||||
|
||||
if (process.env.DEBUG && error.stack) {
|
||||
message.push(error.stack);
|
||||
}
|
||||
printErrorMessage(message.join('\n'));
|
||||
|
||||
if (error instanceof ExpectedError) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Report "unexpected" errors via Sentry.io
|
||||
await captureException(error)
|
||||
.timeout(1000)
|
||||
.catch(function() {
|
||||
// Ignore any errors (from error logging, or timeouts)
|
||||
})
|
||||
// exit with the process.exitCode set earlier
|
||||
.finally(() => process.exit());
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
_ = require('lodash')
|
||||
Mixpanel = require('mixpanel')
|
||||
Raven = require('raven')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
packageJSON = require('../package.json')
|
||||
|
||||
exports.getLoggerInstance = _.memoize ->
|
||||
return resin.models.config.getMixpanelToken().then(Mixpanel.init)
|
||||
|
||||
exports.trackCommand = (capitanoCommand) ->
|
||||
capitanoStateGetMatchCommandAsync = Promise.promisify(require('capitano').state.getMatchCommand)
|
||||
|
||||
return Promise.props
|
||||
resinUrl: resin.settings.get('resinUrl')
|
||||
username: resin.auth.whoami().catchReturn(undefined)
|
||||
mixpanel: exports.getLoggerInstance()
|
||||
.then ({ username, resinUrl, mixpanel }) ->
|
||||
return capitanoStateGetMatchCommandAsync(capitanoCommand.command).then (command) ->
|
||||
Raven.mergeContext(user: {
|
||||
id: username,
|
||||
username
|
||||
})
|
||||
mixpanel.track "[CLI] #{command.signature.toString()}",
|
||||
distinct_id: username
|
||||
argv: process.argv.join(' ')
|
||||
version: packageJSON.version
|
||||
node: process.version
|
||||
arch: process.arch
|
||||
resinUrl: resinUrl
|
||||
platform: process.platform
|
||||
command: capitanoCommand
|
||||
.timeout(100)
|
||||
.catchReturn()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user