mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1879 Commits
v10.13.5
...
output-fra
Author | SHA1 | Date | |
---|---|---|---|
ab1d8aa6ba | |||
d2330f9ed1 | |||
cc19b00998 | |||
ed5ac75a10 | |||
465b8a1b5e | |||
eccadbdcb9 | |||
31eb734af1 | |||
fa7b59d64f | |||
1e42bfa0d5 | |||
5464e550e7 | |||
c0f27a663d | |||
d1c61c62ab | |||
a9691bff57 | |||
f5d09a43cd | |||
d11e547e11 | |||
bd462aee02 | |||
f633c0468b | |||
e4f61a1242 | |||
96142a002e | |||
6b9a5cd89c | |||
ba2d3d60ec | |||
d1e66bc1a5 | |||
58799915a9 | |||
5f2d55f569 | |||
8d6e51391c | |||
8454b02988 | |||
879d98ef98 | |||
c4e317a290 | |||
7ca4d2d720 | |||
e1e88ec56d | |||
33f7fa3829 | |||
3d516e7c5f | |||
a8507508b7 | |||
008972b3d3 | |||
92b86330a0 | |||
2563c07c6a | |||
1d4b949cf3 | |||
d17e02a930 | |||
a355cbaa79 | |||
bd021c0a2d | |||
a80f676804 | |||
f723c58089 | |||
e27a4e2e31 | |||
b91b72c408 | |||
5cf84d3f1d | |||
7d58b8c120 | |||
851301a336 | |||
ec6fd050f6 | |||
6f81053882 | |||
dbd8a9a08c | |||
256f1abf1b | |||
acd352cb3c | |||
31f927c27c | |||
3d0f16168a | |||
b2d932afab | |||
398175f0b3 | |||
2fb9c6c773 | |||
66608b32e9 | |||
c403683edf | |||
1e6ab46ca3 | |||
02d3220f2d | |||
c86cdc8f84 | |||
84f02dc063 | |||
9145f2fb28 | |||
1164388d78 | |||
06f6094401 | |||
67e11467f7 | |||
c8dfd0ca65 | |||
8b110a835a | |||
7564d95f82 | |||
f12f2b79ef | |||
176d731f9e | |||
1ed39d1d37 | |||
580ca0d584 | |||
73572df7cf | |||
23b42b1a2b | |||
632322e3c2 | |||
4faa5d7f57 | |||
9b967592a9 | |||
e01483cd2b | |||
6d89ff4bbf | |||
126e731117 | |||
32d26ad074 | |||
2bcfec9d0f | |||
c04e63ab7d | |||
79be06820c | |||
ffb94c380f | |||
385b5e9ec6 | |||
8d3a4343cb | |||
6eeb16245b | |||
3961060f90 | |||
a6dfc9126a | |||
e7ddd07b7b | |||
fea351d960 | |||
40e0b2dbed | |||
3def4d0e4a | |||
aa286cc0e7 | |||
8abeb6aed7 | |||
f285880135 | |||
2b5c387313 | |||
8babf4c908 | |||
bfc995e948 | |||
c6a0bc0fba | |||
ae69accf0f | |||
cfcace4c99 | |||
6e07db0813 | |||
5c40c8d51f | |||
d827005154 | |||
76081343cc | |||
f3fb9b6bdf | |||
c125e0b38d | |||
73b2f6b4b1 | |||
fdc0d08e96 | |||
e431a59af7 | |||
41a2dbe60c | |||
6ba67eefdb | |||
3b885ad906 | |||
5574dc0318 | |||
fcea91bfb6 | |||
7316c4e075 | |||
389b7a1463 | |||
09d004423c | |||
97978ff812 | |||
498e21f0ab | |||
257dd514ed | |||
85cbdd4947 | |||
73625611da | |||
d2a5a9ba86 | |||
1cd78215e0 | |||
6d744d0b07 | |||
9d312bcd12 | |||
e22aa847e3 | |||
0d1ca67d5b | |||
c4a5a25f03 | |||
b183d88400 | |||
2b6a2142eb | |||
58b29bf4bb | |||
fc0903a414 | |||
cea23f5d5e | |||
5a9b5e3b08 | |||
52138d41eb | |||
5acdc63068 | |||
b546e4dd97 | |||
e4870916e2 | |||
3ca93448cd | |||
f66395e2d5 | |||
952d782e90 | |||
d53c9b3c50 | |||
2f706c0200 | |||
d64b6deb81 | |||
55fc9b2ade | |||
6c29d0ae27 | |||
f46452f6de | |||
c166ec7597 | |||
7325c79888 | |||
2a29b386eb | |||
23b07f8a41 | |||
6d641b4841 | |||
7b498149b1 | |||
ae5ea0f4e8 | |||
f635f648da | |||
3d4e2cf823 | |||
ef3b630887 | |||
19040ccb6c | |||
8e712ac910 | |||
c401ed35ac | |||
94be97313b | |||
48053ecefc | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 | |||
485a9e944f | |||
1d7a50f007 | |||
64a44e7a5f | |||
c3406603db | |||
f1fa187a58 | |||
6cb2893750 | |||
216172ed4f | |||
3717d8cc0f | |||
8338e2e933 | |||
918c2e912d | |||
be0622ec80 | |||
07eef7bb49 | |||
0892caa155 | |||
fa4e8e7b55 | |||
e624726e44 | |||
f914fa2d8a | |||
c8f5542c8a | |||
a2cad7bf53 | |||
3a871a0003 | |||
e552e36f7b | |||
c325f1158e | |||
f79ccc0c95 | |||
1ec8d9a4ca | |||
427b0d9b41 | |||
cfd790a193 | |||
36f4c1312b | |||
fe7cbf4f74 | |||
4e8b8fe582 | |||
2986e6cea3 | |||
bb6b4b255a | |||
350c4abb96 | |||
fec96b41ee | |||
1dba5cc7c1 | |||
43c6fe672f | |||
486cae1aaa | |||
4d588e51a7 | |||
0035545ce1 | |||
d559b9a5a1 | |||
e2ffc5f068 | |||
75b2fa0e9e | |||
c619ecd41b | |||
7ed01a925b | |||
460022a7cf | |||
d15b54cf40 | |||
c938df2445 | |||
bf1df05606 | |||
e04242db64 | |||
9265588745 | |||
cd8070b1a6 | |||
b17dad8c60 | |||
a254e46118 | |||
6e7a0defb7 | |||
492dbae7f1 | |||
ccc2c20b6d | |||
501882fd26 | |||
e2ff561728 | |||
5544f4a5dd | |||
a3e90182bc | |||
3ac85dcc5f | |||
1ac573c659 | |||
2c922ee6d2 | |||
d9821939d9 | |||
732fc2d539 | |||
535a443d7c | |||
579d68a8f0 | |||
ffac8cb9e5 | |||
0f2780744f | |||
b4495839ca | |||
f45ac42dd3 | |||
112a7b8194 | |||
fa26004648 | |||
ba1ea54d69 | |||
9fb62d92b7 | |||
8780a24fb5 | |||
3d3e91d49d | |||
f6e6d9ce8b | |||
0f9d78ab50 | |||
06f7683837 | |||
83a23d9f30 | |||
ffa181a2c3 | |||
d50d18d492 | |||
0b0fb94834 | |||
c1244c0c98 | |||
213e54feb1 | |||
cc8a8513e9 | |||
42c3236313 | |||
91fd515266 | |||
57cd096612 | |||
854501cf8d | |||
d44afa8c39 | |||
b7500fc2c2 | |||
dc6c8d7472 | |||
5c5be8f7b7 | |||
5bdd6c6034 | |||
a5bade99fc | |||
9c3eb76856 | |||
973f1a9c40 | |||
16ea0c9d6d | |||
73bfe545e8 | |||
f53e658ca2 | |||
b66706e8ee | |||
11e50466d5 | |||
431c4b6e4a | |||
d12490f816 | |||
67b7b8b5d0 | |||
16d1f0f06f | |||
9676ea94cb | |||
df8ce0bbe0 | |||
6437bb7511 | |||
ac96616e4e | |||
2737c9c53c | |||
3b8a46f523 | |||
3ac1994941 | |||
b3a6c6cb0f | |||
6d4faa7b2c | |||
9036ce9af3 | |||
4911db640f | |||
e7999f52a9 | |||
68b61e7424 | |||
329b84d01e | |||
25b1dff5d8 | |||
fb1768b4ca | |||
cbc1e52256 | |||
37c2880996 | |||
835445be2e | |||
52fe7481fc | |||
88072173d0 | |||
fdc2bff063 | |||
4f6f20f469 | |||
50af0760ce | |||
43906d22c8 | |||
43f1188f1d | |||
2629a01c7f | |||
5fc009a6ae | |||
480f84993b | |||
d1fdbd927e | |||
4bfd345b68 | |||
d4a153d2ee | |||
3cff091e3a | |||
b2ad9f1643 | |||
f7623bef85 | |||
af63794571 | |||
65d5bdff08 | |||
23165806aa | |||
3649bafbb1 | |||
c62445a399 | |||
b233ea3e3e | |||
4fe660b3a5 | |||
1f07cd1b1c | |||
bcea5193a1 | |||
8b99cd7170 | |||
1986c9339c | |||
b90c9b0d7e | |||
e28c3f9814 | |||
d054ced541 | |||
c8e4d2c9a6 | |||
9671372b9e | |||
2a4ff75203 | |||
f3d750a024 | |||
a701cd8d4d | |||
e2c0c2f359 | |||
15fc805f89 | |||
0a995ecc49 | |||
1ba992ada2 | |||
e47fd0c887 | |||
af1de34840 | |||
96fb525378 | |||
3d1f16c0ab | |||
6fb58a25fc | |||
e6b85c9cf8 | |||
43b93e7fd4 | |||
a05dcf08b8 | |||
9636985ee7 | |||
023fc57914 | |||
492bdab2fe | |||
941c365259 | |||
fed58278c9 | |||
d74af38bfe | |||
53926067ca | |||
7181dc5401 | |||
e35e13f9a7 | |||
6e0638f3be | |||
d60ec13d5c | |||
731e50a757 | |||
b363d28664 | |||
7ae83d9ce5 | |||
31281549a6 | |||
e86bcc438c | |||
a1cf602f6f | |||
4cd3ef8b91 | |||
e4eb4586f5 | |||
360c6e42f8 | |||
f76702c4e0 | |||
d3586696b4 | |||
f73e3db4de | |||
1f74889386 | |||
743de66138 | |||
8d56fe9678 | |||
3d9d8bf5c8 | |||
8c3df9ae30 | |||
e71184ed3a | |||
caadce6c2b | |||
f45fac6138 | |||
aeff5997d0 | |||
b5028c65cc | |||
f69276e7c9 | |||
9fff9266d4 | |||
0e7f953f72 | |||
61b11994b5 | |||
1e1935cfb1 | |||
27e2b03702 | |||
358acbd2c8 | |||
b040a21268 | |||
074fe010bd | |||
34557e35ee | |||
3bff569758 | |||
cf06a8dfad | |||
584aa745f7 | |||
194d12cb3d | |||
7739379444 | |||
5c93df921e | |||
da652c6bce | |||
1cd341e6cd | |||
9d2884aab7 | |||
f128eaf389 | |||
70b0524eb6 | |||
c898747468 | |||
6fc3b0df58 | |||
746676beb9 | |||
611f59a0da | |||
c6430274e5 | |||
9637f75617 | |||
439d8391ee | |||
0d3ca63f00 | |||
1f3677bdb2 | |||
10bca728f0 | |||
9763a14e97 | |||
fe24280adf | |||
a11f9ec705 | |||
836ae1cf4a | |||
b4d37e7a3a | |||
055ad834e7 | |||
d2cb88dfb8 | |||
d096743e78 | |||
511d0dbe26 | |||
6b0201866f | |||
9e20b2b691 | |||
665e0cf9d7 | |||
b319ec7281 | |||
ae3ccf759f | |||
309b1ba6a0 | |||
532c4a1862 | |||
fc8b7c71fc | |||
07666e953f | |||
54731c2d20 | |||
d00db5ea8c | |||
5497835728 | |||
5bb05f3a8c | |||
659eda8cd1 | |||
a19132d3bf | |||
140993f554 | |||
575eaf6de1 | |||
3edf7a038f | |||
ad16c5270e | |||
adadefdf3f | |||
19fab40398 | |||
4dc53eb056 | |||
9c96da7515 | |||
8a3e386d21 | |||
5eaa4cfb9f | |||
cb2b90732b | |||
090fc58d10 | |||
3b05971098 | |||
aae6aff3e9 | |||
0bae6546f2 | |||
40ab27df26 | |||
7d5a64f59a | |||
8115d156df | |||
08fc1a3924 | |||
950d173d27 | |||
ac49246141 | |||
0689074dd7 | |||
ee79c87723 | |||
9dc9556619 | |||
2f9212d622 | |||
2bf59530c4 | |||
a4fd7d6118 | |||
65f053dd6e | |||
8137b79078 | |||
e9b5773bcb | |||
4768f76385 | |||
d6b3249274 | |||
02a5466746 | |||
0831e5fa17 | |||
4681d901f8 | |||
6a55613199 | |||
893a39e891 | |||
fa4f91e08d | |||
54dc37dbd3 | |||
1b0c14feab | |||
20e0810d2a | |||
edc2e77ddd | |||
7da9a800cc | |||
2ba4405452 | |||
e7ebf1ad12 | |||
46249e319b | |||
fcd0932df8 | |||
34792ecce9 | |||
1e18096873 | |||
4da1ed3a56 | |||
92b8741288 | |||
6b4c28a026 | |||
849fc24158 | |||
16efb9748f | |||
9d177609f5 | |||
826b0659d6 | |||
46d7d1d068 | |||
47fcffe368 | |||
bb7cd7ac62 | |||
a83f6c95df | |||
7f000ee8c3 | |||
e5e7bb4757 | |||
37e6bd4b5c | |||
c48564e85a | |||
8460dac066 | |||
64ffcfdd91 | |||
077e25ebc4 | |||
709f009f9b | |||
116ab1fbc1 | |||
260a30532a | |||
7534042519 | |||
6b208ec2ab | |||
099d755900 | |||
3199f15662 | |||
4c8dc29946 | |||
2b22fb89f1 | |||
cf7d9246e5 | |||
0d3106af0e | |||
478b5dd363 | |||
0708608c7e | |||
c245dc70c2 | |||
4373ba7a5d | |||
2cc8d15c05 | |||
592efd0a2e | |||
31123d28f0 | |||
9b6ffecaba | |||
d0e4fa0e59 | |||
cf376316bc | |||
8f0f3bda29 | |||
c33409adb0 | |||
873eb1fc59 | |||
af70f16a9b | |||
e8d757ca28 | |||
63d3402924 | |||
8a506bc4c0 | |||
a14d89fe10 | |||
29ed0a232d | |||
8978221866 | |||
2974c203b5 | |||
c85acbd90b | |||
8a808e25d0 | |||
75687f51ac | |||
eddbdfe0dc | |||
d8acc3f814 | |||
fc8be3d8dc | |||
0ee02a4d73 | |||
568fcb9759 | |||
6133bb2096 | |||
48076464da | |||
1acf342fb0 | |||
340ca6577b | |||
0a8b3ce4e4 | |||
65c01ac172 | |||
4c9a22aba7 | |||
889fafcffc | |||
719cc2e4c9 | |||
e484701276 | |||
b1897a512d | |||
f98c25eaee | |||
b9c3b57b85 | |||
8aff330516 | |||
abdaf0043f | |||
960cb3098d | |||
e907f12445 | |||
799e0f9dea | |||
c389f41006 | |||
74ca5207ad | |||
3706db2436 | |||
6ec0b4a3bd | |||
e65caed64e | |||
b180eb7b73 | |||
9805854eab | |||
00c956394d | |||
b3510f205f | |||
e755d9f03f | |||
f9224b05af | |||
ece4d88bfd | |||
0dd7c33237 | |||
cd20f1765e | |||
0ca1faba09 | |||
9f8569e33f | |||
d7007721a7 | |||
f9f1863fdb | |||
93e18bea27 | |||
73f49765ec | |||
3a508dc397 | |||
bd5bf0135a | |||
e0c65bdef8 | |||
b9d90b9e38 | |||
d910319ba5 | |||
5e5a2c1c85 | |||
238c371ade | |||
504877c232 | |||
bdcf58471f | |||
46b9c586a6 | |||
273ea5ce4d | |||
d56fec6e36 | |||
cd81ff005f | |||
dee216eeaa | |||
d1539f405a | |||
d131fb4fa8 | |||
a0380848a0 | |||
ffa8e245ba | |||
8631e22686 | |||
f0bd3a38db | |||
88569066b5 | |||
c20bbe658b | |||
ac0ce8f702 | |||
42c6e1010f | |||
1f4554abe8 | |||
4e457da5a9 | |||
2e1570149d | |||
c647989054 | |||
44bd667648 | |||
2d042ee116 | |||
787966a0b6 | |||
a59d85e833 | |||
d0616acf1b | |||
d21a18f353 | |||
7d3dbc2c0b | |||
529b98552c | |||
99a478ee39 | |||
fb879d3020 | |||
4fb4cce842 | |||
f772957d29 | |||
fd9520224c | |||
c1afaa6cf3 | |||
8cb413c1c9 | |||
e96fca551e | |||
edb3ea53fb | |||
358a909214 | |||
eb74ca631a | |||
64ebebb121 | |||
abc62404ab | |||
af1c4b0d03 | |||
830e1f801d | |||
59c398fbf0 | |||
d7f49d2442 | |||
34597f629d | |||
3fa7eec8a9 | |||
1ee12b70bc | |||
ca7b1ae084 | |||
936d3cb62a | |||
230677e5e8 | |||
025f817eb6 | |||
54cceb688f | |||
648a73fd91 | |||
3691ae148e | |||
4496bc88f5 | |||
afded27692 | |||
c1a5718364 | |||
e021ad9af6 | |||
5c8a5165e0 | |||
71ff73c641 | |||
c35472e94d | |||
511bb05cb9 | |||
53b2b54b23 | |||
e7f753007f | |||
0afaf8502f | |||
3272b55dd9 | |||
4c664167f6 | |||
604c182e2c | |||
60593a77ac | |||
497c8cd49b | |||
d348d9f71f | |||
d6651fdd7e | |||
e1c42405a1 | |||
bf22d9eaa8 | |||
88523a2887 | |||
e8eb031253 | |||
120c82d657 | |||
cb2e60d5af | |||
62dfae371c | |||
eaf220b64f | |||
9804dd3c33 | |||
94f3825119 | |||
abde3cf48a | |||
efb488f81a | |||
6ca7c34e57 | |||
3f084366db | |||
9f98529e56 | |||
bab98df87b | |||
4d9affd030 | |||
15b536a3b2 | |||
505acc19db | |||
1d566a72ca | |||
0337e284a6 | |||
74c6f8a627 | |||
7aa1708f46 | |||
32a21684e8 | |||
a52a623fdf | |||
b63e31e255 | |||
fec01977c7 | |||
cf894d98a5 | |||
d18f25cb9c | |||
4cdff9694e | |||
304ade9772 | |||
0865633020 | |||
ddb87f403d | |||
8047779c0c | |||
9da7f03b2a | |||
9aacb7ec56 | |||
41e7ba12ff | |||
10decc785d | |||
47e9d39c6f | |||
0eb4e6d770 | |||
492b877d02 | |||
09b8cc495c | |||
94cc84e5ce | |||
cd3fa4521c | |||
5d7d687d6c | |||
5b39878d54 | |||
2c24d80492 | |||
0f058a4441 | |||
66f793e327 | |||
60917f641b | |||
56cc70cd50 | |||
fd4c740d29 | |||
644e643fab | |||
f6529eec26 | |||
401c116820 | |||
4e37ce14c6 | |||
3fc25cd745 | |||
598423acaa | |||
1685ce16f4 | |||
d4de25e545 | |||
d1f46a59f7 | |||
29854f8737 | |||
7eb398c6ef | |||
9ed3bb2f70 | |||
839de65df2 | |||
b2c680e824 | |||
b7c74598b2 | |||
7b78a777ac | |||
82026897c7 | |||
0534ab1cb4 | |||
a6a8dc822f | |||
b0dce6b477 | |||
b9734b7b09 | |||
24ba9da64b | |||
edaf76e329 | |||
d419ae9183 | |||
3833dc3adc | |||
5d3625f6ae | |||
1c17572db0 | |||
c65dafd2ff | |||
33fd24b981 | |||
41635c746b | |||
8c12510f17 | |||
50a658f2f6 | |||
4f831ef443 | |||
457eff1d43 | |||
9e7a13a992 | |||
d9cf95149a | |||
bcc9f9e766 | |||
1ef13665bc | |||
da6c048a3b | |||
3beaa76ec5 | |||
7b4a9a8bc6 | |||
7ebc94c1e5 | |||
92d37ffcb7 | |||
116dc3e623 | |||
17a9d71e59 | |||
6fe8fb7c6e | |||
7da7ff6cf0 | |||
48430fcaee | |||
477c48f831 | |||
fb9943a375 | |||
00c78dfcd8 | |||
c0ca1500eb | |||
88b7b9273a | |||
0c3790bc45 | |||
9069cd3d73 | |||
d6f1328238 | |||
463f3f40ca | |||
38fe9042e5 | |||
6f265b24aa | |||
e2c9bf86c7 | |||
73ba4a1283 | |||
eea8c83bff | |||
0bd27bd7ac | |||
b15f4de899 | |||
5242510481 | |||
6082771aad | |||
bf8f41d2b4 | |||
b1ab0337c0 | |||
a27536045d | |||
3b56b7b324 | |||
11afbf3da2 | |||
0da5f822e4 | |||
9c6b6b3718 | |||
3d7833a640 | |||
4b110fef8e | |||
89e2bb5945 | |||
f326994f8e | |||
e80697013e | |||
3a06701747 | |||
e20265518d | |||
d4c513fd41 | |||
dec92a3d9c | |||
4cdaf29348 | |||
7301b9c246 | |||
1accb885a9 | |||
497023965f | |||
95f9443c2a | |||
f677156fe0 | |||
edeea18b15 | |||
67d6b59cb9 | |||
9997b29ef7 | |||
224f721a72 | |||
467a923353 | |||
20dec8821f | |||
69e9222703 | |||
238e5cb9d2 | |||
f846ef0c19 | |||
8044bb6079 | |||
afc660762f | |||
5002017de6 | |||
2ba9f9dfcf | |||
40f643c07f | |||
402e48c8f8 | |||
6f6cb42725 | |||
a3c10ded82 | |||
178c3f9154 | |||
a74815c1bb | |||
9dbdf7db38 | |||
c35f701190 | |||
9e465217b2 | |||
d20beae926 | |||
2307a15b10 | |||
fe5e6a1dae | |||
33add41dc7 | |||
099ac8b233 | |||
f6ca5ef397 | |||
15ed67422d | |||
d6695d7278 | |||
9697081d73 | |||
35200697de | |||
4fc62017ae | |||
303c3af061 | |||
984d1a3fd6 | |||
ef9c2e9c18 | |||
d4f288394b | |||
41a8e1b2a4 | |||
2fc50eecf5 | |||
5e94f3f9c9 | |||
3d0a93692f | |||
7987e20775 | |||
209ea64b82 | |||
21c8a38c22 | |||
63e30a025e | |||
d0a4ff0d6b | |||
c95568b5a4 | |||
a252a550ff | |||
2a3479c7c6 | |||
7f2501439d | |||
137349b2f4 | |||
7ee59b56c7 | |||
c5628d0056 | |||
a76d49243c | |||
488d9b901c | |||
812b11db5f | |||
d681dacc6d | |||
3207a2922a | |||
45f131ec93 | |||
bd4d836a31 | |||
a039ec86dd | |||
f9a05fe257 | |||
6add63c6ac | |||
361e54b3bd | |||
0ce035f379 | |||
eaf61d801c | |||
4cc4a22af3 | |||
6403e29d76 | |||
1308b64c67 | |||
17089a35c3 | |||
e34dca0e36 | |||
512cfa3da1 | |||
674dccef21 | |||
5b95a16fe1 | |||
667358b23e | |||
f47a4ccb2b | |||
ece3c06786 | |||
b50fef8cb4 | |||
4f7ea7e32a | |||
01c12dd5bf | |||
b114697cab | |||
14ce4d73b6 | |||
051e1f1331 | |||
c054d55456 | |||
133daefe83 | |||
232e9c3fc1 | |||
03eed32f12 | |||
e1d51305b0 | |||
af071beef0 | |||
6dce5c1212 | |||
a26c3912d3 | |||
f3d86ab37c | |||
420a282bea | |||
cfbf00f543 | |||
83888210d6 | |||
2f7dd54e37 | |||
ab9dabaf77 | |||
9e98e7142c | |||
4656f8f11d | |||
5b5f258685 | |||
7d2e32777f | |||
10120bb97f | |||
98c9469151 | |||
98c0991fe3 | |||
f686d3a9bf | |||
8dda8d89d5 | |||
7794158062 | |||
24d6b7fa38 | |||
f5f0c7b4a7 | |||
11d1a3f5a0 | |||
2859d16b31 | |||
70bfe4ce8e | |||
6f4db3176a | |||
ebb0ec5287 | |||
4995e9b642 | |||
795057338f | |||
ab673f884a | |||
907508bce1 | |||
599b897721 | |||
146de39b83 | |||
a3cab32b4e | |||
d709e06f48 | |||
98f101643d | |||
c619bd4b99 | |||
19c3069b22 | |||
7e1d58546c | |||
2c01f8adee | |||
3ecf461d55 | |||
06ab84fd10 | |||
a7b78d2ccd | |||
432109060e | |||
b32ae4a667 | |||
36e4b3249c | |||
41e5fdbe27 | |||
6dc0fe10bc | |||
656591fde4 | |||
d967b942e0 | |||
7e34fdfeeb | |||
995f8a3338 | |||
ff282205d5 | |||
be144fafa2 | |||
683037cd2f | |||
555096db6b | |||
a85c482416 | |||
523d563b4e | |||
1569915fae | |||
b1552f8e9b | |||
f455602c73 | |||
3e97669b3c | |||
728c4f4296 | |||
bf073942f0 | |||
b9290f4859 | |||
626d328194 | |||
21dd959344 | |||
5c8d822aee | |||
2ab8ae1c10 | |||
fcc13f9476 | |||
a38b41f339 | |||
2fc0728a09 | |||
040c4987fc | |||
254d9c49a4 | |||
6e5e1c4f5f | |||
d7213e868f | |||
d82b019480 | |||
1693bd91c0 | |||
e4f605d6ac | |||
fd7e7f57eb | |||
1d073af31a | |||
fcaaec1fff | |||
ac3a688d46 | |||
979284b071 | |||
bc4aa6006e | |||
2cad44915b | |||
889c7b08cf | |||
56a196210d | |||
a23759a1ba | |||
ba0024645d | |||
3cb184c8af | |||
644d54a113 | |||
a6f905b71c | |||
3b426e4a53 | |||
d241523d93 | |||
1c354c800b | |||
e5861a708e | |||
6a019af25f | |||
8522363cd3 | |||
480228d8f4 | |||
175413af34 | |||
d1b4560b37 | |||
77f3fa4b6c | |||
a21d3fe2d2 | |||
df440f0580 | |||
92bfa574e3 | |||
d33b7ec585 | |||
08d5a77734 | |||
744122b1b8 | |||
c3a8bb3de6 | |||
e50d92727e | |||
3bb5e495a6 | |||
803a9070fd | |||
a84ab793a0 | |||
81269e92d5 | |||
11d5deef4c | |||
c98bc3280d | |||
8c2a40cb39 | |||
2fdd023a64 | |||
aff370e9c3 | |||
be21c8d43e | |||
5b33826309 | |||
052c8d138e | |||
d0228f20fd | |||
4577d72ead | |||
0dde84ec0b | |||
81b620f55e | |||
4b056b4d4c | |||
5723e69267 | |||
2d341cac48 | |||
4e50d08f7b | |||
aff5cd9b0d | |||
2bb0933a42 | |||
8d60cd1f92 | |||
9756efb539 | |||
61ed6ff69d | |||
127560fa65 | |||
2611ea22f9 | |||
d7021a556e | |||
9412a21d40 | |||
f63be0b4cd | |||
c2561938c1 | |||
98a2c0635d | |||
ee54d638ad | |||
d9b044c1b8 | |||
dd20a8b00f | |||
01147c31a4 | |||
2f6889cca1 | |||
83286e6729 | |||
97def08ec5 | |||
1a57385626 | |||
1301f62981 | |||
6ae337db8a | |||
b84cdd6230 | |||
2f24e591ef | |||
597b894917 | |||
3b53b75626 | |||
9b1c3c665b | |||
153cdf4bb0 | |||
1d9a397f71 | |||
6167c7b8b3 | |||
cbcd7694a9 | |||
52bece7f17 | |||
4f6550e7eb | |||
ec17ed6ef2 | |||
0df6368ab9 | |||
b5cac122cf | |||
ae75e1396e | |||
3b519f0258 | |||
275fa9c16b | |||
a00db0f5d8 | |||
2a8eb3a6ed | |||
bcd49e0292 | |||
d8e1cd6597 | |||
52c2b041da | |||
d5d0486c3f | |||
6f51807e8c | |||
ab526c9ed8 | |||
14c5b27cdd | |||
ce01ce73b1 | |||
0f6d160b2e | |||
6d7d1956ea | |||
692eddf43f | |||
eb5cfecfaf | |||
73d6d7b264 | |||
14ced9f384 | |||
4d8cd1cc46 | |||
dbe9a727d5 | |||
8ac65c3800 | |||
4ae91ef846 | |||
7311cfa755 | |||
a200bf268d | |||
d398e22c58 | |||
b51d2fffbb | |||
5fef98bdf8 | |||
203ccaf97b | |||
a348528ed3 | |||
04c4250fba | |||
a97398950e | |||
f55376df32 | |||
3f285cc26d | |||
6d95c5bad5 | |||
6b33f95661 | |||
9ab34c2deb | |||
db247307db | |||
ad0b667bc7 | |||
d98bc9fb06 | |||
74cdd80b51 | |||
5c39952002 | |||
2874a69d7d | |||
6ec05e8dcf | |||
6602845202 | |||
e8cd4153c7 | |||
0cfa1a0dfb | |||
00ce3ab751 | |||
5c1323d583 | |||
d9f42b888d | |||
0db8c85fc8 | |||
bc601d07e3 | |||
e1a91035ae | |||
8dced8afe2 | |||
ffded6736a | |||
1a851f552e | |||
68b64016ab | |||
edac54ccfe | |||
560b0abbe7 | |||
b48d238be6 | |||
a10d5b9abe | |||
23f2242e22 | |||
f8612fd748 | |||
36446ff488 | |||
a5ce0436c7 | |||
3302e2f639 | |||
c3c1c5fc41 | |||
9f59b6dde5 | |||
8be56ef092 | |||
e9f8cadb73 | |||
3e4f9f9572 | |||
f7d4a37060 | |||
d0e268815a | |||
c3454d3abb | |||
6b0f645094 | |||
ada7801a0d | |||
da5e26f37e | |||
9447195c26 | |||
cd59496f11 | |||
9fda165d34 | |||
fe0ad92b43 | |||
81c5a62380 | |||
ebdd04ec73 | |||
e6264ced7a | |||
247f31a3cc | |||
a2b761ec4b | |||
e3672bc655 | |||
028141c0b0 | |||
e3c42cf63e | |||
0ae138db03 | |||
9350af9ddf | |||
88e4009e88 | |||
cb7692690d | |||
82e17cea6a | |||
9ed363da9e | |||
8aa4bd6173 | |||
5f098e7410 | |||
2de33d185a | |||
66b9f5a337 | |||
bbcb3a702f | |||
57d0014e32 | |||
8d9133e6a6 | |||
be82bcfa63 | |||
7c9a23451b | |||
1319e0642b | |||
e3b6db25d8 | |||
655534469a | |||
a8b0573699 | |||
99963cbb89 | |||
92715c3182 | |||
264c8535b4 | |||
159ee44d7e | |||
7e4b62c28a | |||
52b2ba6a30 | |||
cc1ba3d84e | |||
01d05fb148 | |||
cff9e50a22 | |||
eba2e7e4fb | |||
9a9d56b419 | |||
320b4864d9 | |||
7f79451376 | |||
68fa831843 | |||
3aa72dde4c | |||
f72d78954d | |||
cf87ca95a0 | |||
a50ca78eef | |||
4fe5a10029 | |||
9812239862 | |||
bc3fe29624 | |||
7e2ee7ab93 | |||
f151a208e5 | |||
292ad89b7e | |||
c062e6e876 | |||
dcb1c11700 | |||
96e28f3d45 | |||
ff319d67f3 | |||
f14e44a2e8 | |||
9aa6b0bc57 | |||
cbe12d5be7 | |||
c177f222ba | |||
d2fd1ec80a | |||
77873cf919 | |||
07c09c5f89 | |||
159cb752d1 | |||
a74f0413df | |||
43b1c5c24f | |||
45e7e9cb32 | |||
1a71bad8bb | |||
2d55df4704 | |||
6c0b3a5e53 | |||
3e955f3a91 | |||
30738d93b0 | |||
be76b8adbd | |||
d6a065a230 | |||
7b8e86372b | |||
bc15ad6e05 | |||
fcad35402a | |||
49b00e18ae | |||
a6ccd87069 | |||
0c1904fbdb | |||
e5d2661c96 | |||
eca3e91512 | |||
eb5ad08649 | |||
b3b22d6399 | |||
217cba819a | |||
c8275b52c3 | |||
47e85da789 | |||
c8cade95da | |||
a4de7143b1 | |||
6574745a23 | |||
1ee74df67e | |||
3e1b10007a | |||
448211e49c | |||
8658104647 | |||
6ec8bcddaa | |||
d138c40ebd | |||
f24c4a036c | |||
dabe81c31b | |||
c2f0f9a894 | |||
46b695cf22 | |||
9b79f79bac | |||
dddfad9dec | |||
0690554a94 | |||
62ea7518bc | |||
f30e486562 | |||
809a5fae25 | |||
eccb1bd9ad | |||
f859d5025a | |||
18d3ca3413 | |||
a826f16469 | |||
505c3ec7d3 | |||
47fa2a6151 | |||
b4b19637f4 | |||
5f552cf9a8 | |||
e42650f433 | |||
731bd909d6 | |||
2860535c45 | |||
122b5a0655 | |||
ec66c82d3f | |||
09a59ab03f | |||
3d2e109e7f | |||
26803067f1 | |||
7dc3977e82 | |||
10cbf514a2 | |||
e2114f73d7 | |||
2f448951c9 | |||
385d3e107b | |||
d98b2fa72f | |||
c6baa7a908 | |||
daa34feeda | |||
f813dad4d9 | |||
d7633b5f08 | |||
f44c2b777f | |||
bcfba693a5 | |||
08f40c0566 | |||
5a80654305 | |||
d2df2c7b60 | |||
36d3d1256e | |||
b77cb56cd0 | |||
524397fc9b | |||
ec73ee270b | |||
b83431c2e0 | |||
40c559322a | |||
2c5cf9dab6 | |||
ca8272b477 | |||
d6e7359400 | |||
af8d7283a5 | |||
9470e804c0 | |||
00943463a4 | |||
3f6d770233 | |||
c4a6086e9c | |||
4e61c00255 | |||
1713988e94 | |||
fe4e1d09d7 | |||
766695ceef | |||
e50a3270ba | |||
235c13bea9 | |||
62e4930e5b | |||
fb321b8c5b | |||
98152c0b09 | |||
0ab0e417b8 | |||
3642943896 | |||
7c62e34455 | |||
86af954f3b | |||
0c7947e185 | |||
48b281d7c6 | |||
8598223b61 | |||
cdd67e25f0 | |||
eac6bb5e5c | |||
077d1db9b7 | |||
d86f213b68 | |||
cdfd1d124b | |||
28c00696b8 | |||
dec570a6e2 | |||
9067558d18 | |||
4abdd71ce7 | |||
36f2f491b3 | |||
5e750b33c3 | |||
03053e125f | |||
bdc7c0fa39 | |||
ad4981328f | |||
3f35d6fde6 | |||
f2be811e18 | |||
6439aa5552 | |||
977fadab69 | |||
95c93d24da | |||
278d7fd02c | |||
59b9429570 | |||
9e870b08a7 | |||
671dca8287 | |||
a15060e9fc | |||
0738dd1520 | |||
5dbace353d | |||
e773549297 | |||
a1c406a479 | |||
5e196b8f63 | |||
054e59c6af | |||
88a1e413a3 | |||
1a74dcf4cf | |||
d48672fa93 | |||
9a7fcfffe8 | |||
f9ece2ce7d | |||
9d04e616a8 | |||
b8c7f23443 | |||
2b04763ac0 | |||
bff845a0e4 | |||
5076ca7532 | |||
93ba5832d8 | |||
af86ac73e6 | |||
173a48eede | |||
a4b34c109d | |||
69714a646b | |||
a41ef3764e | |||
f1220c6377 | |||
cefb3acc1f | |||
277da3ea9c | |||
99f84c2f6a | |||
a9c0899c32 | |||
8d3fb8fef5 | |||
4de41ce3e0 | |||
4b8cec652a | |||
2dd8e71adc | |||
05d478b759 | |||
9a7a364776 | |||
2cb5e28258 | |||
02e8429155 | |||
467afb3de6 | |||
324a406e7f | |||
17bf061853 | |||
6d543b79ff | |||
85aaf77e44 | |||
83c5684491 | |||
6bc4fbb750 | |||
1da96a0eb0 | |||
be209f1626 | |||
654d1dcff8 | |||
0a03e79d9d | |||
3f84045127 | |||
544f8fb4bd | |||
76997c99dc | |||
f4525bc11e | |||
f732c5bf5d | |||
2bc3348aff | |||
895be0be5d | |||
0f17129c2e | |||
9005affe64 | |||
4502f2a203 | |||
da3c11533c | |||
6acff945ef | |||
b3948d538c | |||
f53a69feb1 | |||
405b92114d | |||
27e1f3f7d7 | |||
1417875110 | |||
f58a49d6c3 | |||
f9743b269a | |||
0f5f65e0d3 | |||
58e7880f1d | |||
041823189f | |||
38194e6175 | |||
c04e9665ad | |||
1e37c97ffb | |||
913f09924a | |||
ceb47e9969 | |||
305755549e | |||
77931b314a | |||
b38b5b0b61 | |||
5cf407b483 | |||
8f6902f4cb | |||
751f67e997 | |||
be1a260af6 | |||
9db6961a7e | |||
b978230f9e | |||
cc5fe60a15 | |||
bbea58a9c8 | |||
56e35f6e9f | |||
95b5ac1c7f | |||
df3e1f1886 | |||
5d34659991 | |||
aca794b267 | |||
cd6072ac73 | |||
bda696ad8c | |||
ef4ee54a00 | |||
a2ca8e8f73 | |||
620a0abf31 | |||
95561864a6 | |||
51adfeaa3b | |||
76447a2177 | |||
a6153869e5 | |||
3466be1992 | |||
95843dd816 | |||
edd755d41c | |||
290c06074a | |||
dd7d9d1570 | |||
c4829153fc | |||
615f24edd3 | |||
a94e6d550e | |||
75044030cf | |||
046743071d | |||
4e95cb0cca | |||
4666019c84 | |||
323c9191b6 | |||
024bf2996b | |||
5210d474a9 | |||
3cce8d822c | |||
65250e431e | |||
29cc75598f | |||
33552724a1 | |||
c88b317143 | |||
658b0a5233 | |||
7fd436cd91 | |||
7c1faa6de0 | |||
90e184ea1f | |||
38920a1c59 | |||
df58ac7673 | |||
630d53311a | |||
b1eda160e8 | |||
a63c766f04 | |||
53325b7c05 | |||
622c510d65 | |||
890bea549f | |||
bb19903826 | |||
33210b896b | |||
c2a0e457c0 | |||
f464597069 | |||
02dcff5b67 | |||
2f4539b4d1 | |||
6c3429eb0c | |||
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 |
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@ -0,0 +1,37 @@
|
||||
# Reminders:
|
||||
# * Matching rules are different to `.gitignore`
|
||||
# * A pattern without '**' matches in the project's root directory only
|
||||
# * Leading and trailing '/' are discarded (it is not possible to
|
||||
# distinguish between files and directories)
|
||||
# * More details: https://github.com/balena-io-modules/dockerignore
|
||||
|
||||
# development and testing tools or IDEs
|
||||
**/*.log
|
||||
**/*.pid
|
||||
**/*.seed
|
||||
.idea
|
||||
.lock-wscript
|
||||
.nvmrc
|
||||
.nyc_output
|
||||
.vscode
|
||||
coverage
|
||||
lib-cov
|
||||
logs
|
||||
pids
|
||||
|
||||
# OS cache files
|
||||
**/.DS_Store
|
||||
|
||||
# balena CLI config and build files
|
||||
**/.balenaconf
|
||||
**/.fast-boot.json
|
||||
**/.resinconf
|
||||
balenarc.yml
|
||||
build
|
||||
build-bin
|
||||
dist
|
||||
node_modules
|
||||
oclif.manifest.json
|
||||
package-lock.json
|
||||
resinrc.yml
|
||||
tmp
|
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Set all files to use line feed endings (since we can't match only ones without an extension)
|
||||
* eol=lf
|
||||
# And then reset all the files with extensions back to default
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
docs/balena-cli.md text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @pdcastro @thgreasi @CameronDiver @hedss
|
79
.github/ISSUE_TEMPLATE.md
vendored
79
.github/ISSUE_TEMPLATE.md
vendored
@ -1,11 +1,76 @@
|
||||
- **Cli version:**
|
||||
- **Standalone install:** yes/no
|
||||
- **Node version:**
|
||||
- **Npm version:**
|
||||
- **Operating system and architecture:**
|
||||
|
||||
# About this issue tracker
|
||||
|
||||
*The balena CLI (Command Line Interface) is a tool used to interact with the balena platform.
|
||||
This GitHub issue tracker is used for bug reports and feature requests regarding the CLI
|
||||
tool. General and troubleshooting questions (such as setting up your project to work with a
|
||||
balenalib base image) are encouraged to be posted to the [balena
|
||||
forums](https://forums.balena.io), which are monitored by balena's support team and where the
|
||||
community can both contribute and benefit from the answers.*
|
||||
|
||||
*Please also check that this issue is not a duplicate. If there is another issue describing
|
||||
the same problem or feature please add comments to the existing issue.*
|
||||
|
||||
*Thank you for your time and effort creating the issue report, and helping us improve
|
||||
the balena CLI!*
|
||||
|
||||
---
|
||||
|
||||
*Please keep in mind that we try to use the issue tracker of this repository for specific bug reports & CLI feature requests. General & troubleshooting questions are encouraged to be posted to the [balena forums](https://forums.balena.io) where the community can both contribute and benefit from the answers.*
|
||||
# Expected Behavior
|
||||
|
||||
*Before submitting this issue please check that this issue is not a duplicate. If there is another issue describing the same problem or feature please add your information to the existing issue's comments.*
|
||||
Please describe what you were expecting to happen. If applicable, please add links to
|
||||
documentation you were following, or to projects that you were trying to push/build.
|
||||
|
||||
# Actual Behavior
|
||||
|
||||
Please describe what actually happened instead:
|
||||
* Quoting logs and error message is useful. If possible, quote the **full** output of the
|
||||
CLI, not just the error message.
|
||||
* Please quote the **full command line** too. Sometimes users report that they were
|
||||
"pushing" or "building" a project, but there are several ways to do so and several
|
||||
possible "targets" such as balenaCloud, openBalena, local balenaOS device, etc.
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myFleet
|
||||
balena push 192.168.0.12
|
||||
balena deploy myFleet
|
||||
balena deploy myFleet --build
|
||||
balena build . -f myFleet
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
Each of the above command lines executes different code behind the scenes, so quoting the
|
||||
full command line is very helpful.
|
||||
|
||||
Running the CLI in debug mode (`--debug` flag or `DEBUG=1` environment variable) may reveal
|
||||
additional information. The `--logs` option reveals additional information for the commands:
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myFleet --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
||||
This is the most important and helpful part of a bug report. If we cannot reproduce the
|
||||
problem, it is difficult to tell what the fix should be, or whether code changes have
|
||||
fixed it.
|
||||
|
||||
1.
|
||||
1.
|
||||
1.
|
||||
|
||||
# Specifications
|
||||
|
||||
- **balena CLI version:** e.g. 1.2.3 (output of the `"balena version -a"` command)
|
||||
- **Cloud backend: openBalena or balenaCloud?** If unsure, it will be balenaCloud
|
||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||
- **Install method:** npm or zip package or executable installer
|
||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||
|
||||
# Additional References
|
||||
|
||||
If applicable, please add additional links to GitHub projects, forums.balena.io threads,
|
||||
gist.github.com, Google Drive attachments, etc.
|
||||
|
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
31
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,11 +1,26 @@
|
||||
<!-- You can remove tags that do not apply. -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
Change-type: major|minor|patch <!-- The change type of this PR -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
Change-type: major|minor|patch <!-- See https://semver.org/ -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
|
||||
---
|
||||
##### Contributor checklist
|
||||
<!-- For completed items, change [ ] to [x]. -->
|
||||
- [ ] Introduces security considerations
|
||||
- [ ] Affects the development, build or deployment processes of the component
|
||||
Please check the CONTRIBUTING.md file for relevant information and some
|
||||
guidance. Keep in mind that the CLI is a cross-platform application that runs
|
||||
on Windows, macOS and Linux. Tests will be automatically run by balena CI on
|
||||
all three operating systems, but this will only help if you have added test
|
||||
code that exercises the modified or added feature code.
|
||||
|
||||
Note that each commit message (currently only the first line) will be
|
||||
automatically copied to the CHANGELOG.md file, so try writing it in a way
|
||||
that describes the feature or fix for CLI users.
|
||||
|
||||
If there isn't a linked issue or if the linked issue doesn't quite match the
|
||||
PR, please add a PR description to explain its purpose or the features that it
|
||||
implements. Adding PR comments to blocks of code that aren't self explanatory
|
||||
usually helps with the review process.
|
||||
|
||||
If the PR introduces security considerations or affects the development, build
|
||||
or release process, please be sure to highlight this in the PR description.
|
||||
|
||||
Thank you very much for your contribution!
|
||||
|
69
.gitignore
vendored
69
.gitignore
vendored
@ -1,45 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# Reminders:
|
||||
# * A pattern without '/' matches in subdirectories as well (files and directories)
|
||||
# * A leading '/' anchors matching to the directory where `.gitignore` is defined
|
||||
# * A trailing '/' makes the pattern match against directories only
|
||||
# More details: https://git-scm.com/docs/gitignore
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
# development and testing tools or IDEs
|
||||
*.log
|
||||
*.pid
|
||||
*.seed
|
||||
/.idea/
|
||||
/.lock-wscript
|
||||
/.nvmrc
|
||||
/.nyc_output/
|
||||
/.vscode/
|
||||
/coverage/
|
||||
/lib-cov/
|
||||
/logs
|
||||
/pids
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# Commenting this out is preferred by some people, see
|
||||
# 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
|
||||
|
||||
# OS cache files
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
/bin/.fast-boot.json
|
||||
# balena CLI config and build files
|
||||
.balenaconf
|
||||
.fast-boot.json
|
||||
.resinconf
|
||||
/balenarc.yml
|
||||
/build/
|
||||
/build-bin/
|
||||
/dist/
|
||||
/node_modules
|
||||
/oclif.manifest.json
|
||||
/package-lock.json
|
||||
/resinrc.yml
|
||||
/tmp/
|
||||
|
@ -1,5 +1,2 @@
|
||||
coffee_script:
|
||||
config_file: coffeelint.json
|
||||
|
||||
javascript:
|
||||
enabled: false
|
||||
|
6
.mocharc-standalone.js
Normal file
6
.mocharc-standalone.js
Normal file
@ -0,0 +1,6 @@
|
||||
const commonConfig = require('./.mocharc.js');
|
||||
|
||||
module.exports = {
|
||||
...commonConfig,
|
||||
spec: ['tests/auth/*.spec.ts', 'tests/commands/**/*.spec.ts'],
|
||||
};
|
10
.mocharc.js
Normal file
10
.mocharc.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
reporter: 'spec',
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
timeout: 12000,
|
||||
// To test only, say, 'push.spec.ts', do it as follows so that
|
||||
// requests are authenticated:
|
||||
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
||||
spec: 'tests/**/*.spec.ts',
|
||||
};
|
31
.resinci.yml
31
.resinci.yml
@ -2,35 +2,14 @@
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: alpine
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
- 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
|
||||
- "12"
|
||||
- "14"
|
||||
|
19
.travis.yml
19
.travis.yml
@ -1,19 +0,0 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "6"
|
||||
before_install:
|
||||
- npm -g install npm@4
|
||||
script: npm run ci
|
||||
notifications:
|
||||
email: false
|
||||
deploy:
|
||||
- provider: script
|
||||
script: npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: balena-io/balena-cli
|
19610
.versionbot/CHANGELOG.yml
Normal file
19610
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
4239
CHANGELOG.md
4239
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
269
CONTRIBUTING.md
Normal file
269
CONTRIBUTING.md
Normal file
@ -0,0 +1,269 @@
|
||||
# Contributing
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
* Install the dependencies listed in the [NPM Installation
|
||||
section](./INSTALL-ADVANCED.md#npm-installation) section of the installation instructions. Check
|
||||
the section [Additional Dependencies](./INSTALL-ADVANCED.md#additional-dependencies) too.
|
||||
* Clone the `balena-cli` repository (or a [forked
|
||||
repo](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo),
|
||||
if you are not in the balena team), `cd` to it and run `npm install`.
|
||||
* Build the CLI with `npm run build` or `npm test`, and execute it with `./bin/balena`
|
||||
(on a Windows command prompt, you may need to run `node .\bin\balena`).
|
||||
|
||||
In order to ease development:
|
||||
|
||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
|
||||
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
||||
|
||||
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
||||
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
|
||||
this will only help if you add some test cases for your new code!
|
||||
|
||||
## Semantic versioning, commit messages and the ChangeLog
|
||||
|
||||
When a pull request is merged, Balena's versionbot / Continuous Integration system takes care of
|
||||
automatically creating a new CLI release on both the [npm
|
||||
registry](https://www.npmjs.com/package/balena-cli) and the GitHub [releases
|
||||
page](https://github.com/balena-io/balena-cli/releases). The release version numbering adheres to
|
||||
the [Semantic Versioning's](http://semver.org/) concept of patch, minor and major releases.
|
||||
Generally, bug fixes and documentation changes are classed as patch changes, while new features are
|
||||
classed as minor changes. If a change breaks backwards compatibility, it is a major change.
|
||||
|
||||
A new version entry is also automatically added to the
|
||||
[CHANGELOG.md](https://github.com/balena-io/balena-cli/blob/master/CHANGELOG.md) file when a pull
|
||||
request is merged. Each pull request corresponds to a single version / release. Each commit in the
|
||||
pull request becomes a bullet point entry in the Changelog. The Changelog file should not be
|
||||
manually edited.
|
||||
|
||||
To support this automation, a commit message should be structured as follows:
|
||||
|
||||
```text
|
||||
The first line becomes a bullet point in the CHANGELOG file
|
||||
|
||||
Optionally, a more detailed description in one or more paragraphs.
|
||||
The detailed description can be seen with `git log`, but it is not copied
|
||||
to the CHANGELOG file.
|
||||
|
||||
Change-type: patch|minor|major
|
||||
```
|
||||
|
||||
Only the first line of the commit message is copied to the Changelog file. The `Change-type` footer
|
||||
must be preceded by a blank line, and indicates the commit's semver change type. When a PR consists
|
||||
of multiple commits, the commits may have different change type values. As a whole, the PR will
|
||||
produce a release of the "highest" change type. For example, two commits mixing patch and minor
|
||||
change types will produce a minor CLI release, while two commits mixing minor and major change
|
||||
types will produce a major CLI release.
|
||||
|
||||
The commit message is parsed / checked by versionbot with the
|
||||
[resin-commit-lint](https://github.com/balena-io-modules/resin-commit-lint#resin-commit-lint)
|
||||
package.
|
||||
|
||||
Because of the way that the Changelog file is automatically updated from commit messages, which
|
||||
become the source of "what's new" for CLI end users, we advocate "meaningful commits" and
|
||||
user-focused commit messages. A meaningful commit is one that, in isolation, introduces a fix or
|
||||
feature (or part of a fix or feature) that makes sense at the Changelog level, and which leaves the
|
||||
CLI in a non-broken state. Sometimes, in the course of preparing a single pull request, a developer
|
||||
creates several commits as a way of saving their "work in progress", which may even fail to build
|
||||
(e.g. `npm run build` fails), and which is then fixed or undone by further commits in the same PR.
|
||||
In this situation, the recommendation is to "squash" or "fixup" the work-in-progress commits into
|
||||
fewer, meaningful commits. Interactive rebase is a good tool to achieve this:
|
||||
[blog](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history),
|
||||
[docs](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History).
|
||||
|
||||
Mixing multiple distinct features or bug fixes in a single commit is discouraged, because the
|
||||
description will likely not fit in the single-line Changelog bullet point and also because it
|
||||
makes it harder to review the pull request (especially a large one) and harder to isolate and
|
||||
revert individual changes in case a bug is found later on. Create a separate commit for each
|
||||
feature / bug fix, or even separate pull requests.
|
||||
|
||||
If you need to catch up with changes to the master branch while working on a pull request,
|
||||
use rebase instead of merge: [docs](https://git-scm.com/book/en/v2/Git-Branching-Rebasing).
|
||||
|
||||
If `package.json` is updated for dependencies listed in the `repo.yml` file (like `balena-sdk`),
|
||||
the commit message body should also include a line in the following format:
|
||||
```
|
||||
Update balena-sdk from 12.0.0 to 12.1.0
|
||||
```
|
||||
|
||||
This allows versionbot to produce nested Changelog entries (with expandable arrows), pulling in
|
||||
commit messages from the upstream repositories. The following npm script can be used to
|
||||
automatically produce a commit with a suitable commit message:
|
||||
```
|
||||
npm run update balena-sdk ^12.1.0
|
||||
```
|
||||
|
||||
The script will create a new branch (only if `master` is currently checked out), run `npm update`
|
||||
with the given target version and commit the `package.json` and `npm-shrinkwrap.json` files. The
|
||||
script by default will set the `Change-type` to `patch` or `minor`, depending on the semver change
|
||||
of the updated dependency. A `major` change type can specified as an extra argument:
|
||||
```
|
||||
npm run update balena-sdk ^12.14.0 patch
|
||||
npm run update balena-sdk ^13.0.0 major
|
||||
```
|
||||
|
||||
## Editing documentation files (README, INSTALL, Reference website...)
|
||||
|
||||
The `docs/balena-cli.md` 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 `docs/balena-cli.md` are:
|
||||
|
||||
* [Selected
|
||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||
of the README file.
|
||||
* The CLI's command documentation in source code (`lib/commands/` folder), for example:
|
||||
* `lib/commands/push.ts`
|
||||
* `lib/commands/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`docs/balena-cli.md` 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
|
||||
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
that produces the `.exe` graphical installer on Windows also requires
|
||||
[NSIS](https://sourceforge.net/projects/nsis/) and [MSYS2](https://www.msys2.org/) to be
|
||||
installed. Be sure to add `C:\Program Files (x86)\NSIS` to the PATH, so that `makensis`
|
||||
is available. MSYS2 is recommended when developing the balena CLI on Windows.
|
||||
|
||||
If changes are made to npm scripts in `package.json`, don't assume that a Unix shell like
|
||||
bash is available. For example, some Windows shells don't have the `cp` and `rm` commands,
|
||||
which is why you'll often find `ncp` and `rimraf` used in `package.json` scripts.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
The `npm-shrinkwrap.json` file is used to control package dependencies, as documented at
|
||||
https://docs.npmjs.com/files/shrinkwrap.json.
|
||||
|
||||
Changes to `npm-shrinkwrap.json` can be automatically merged by git during operations like
|
||||
`rebase`, `pull` and `cherry-pick`, but in some cases this results in suboptimal dependency
|
||||
resolution (the `node_modules` folder may end up larger than necessary, with consequences to CLI
|
||||
load time too). For this reason, the recommended way to update `npm-shrinkwrap.json` is to run
|
||||
`npm install`, possibly alongside `npm dedupe` as well. The following commands can be used to
|
||||
fix shrinkwrap issues and optimize the dependencies:
|
||||
|
||||
```sh
|
||||
git checkout master -- npm-shrinkwrap.json
|
||||
rm -rf node_modules
|
||||
npm install # update npm-shrinkwrap.json to satisfy changes to package.json
|
||||
npm dedupe # deduplicate dependencies from npm-shrinkwrap.json
|
||||
npm install # re-add optional dependencies removed by dedupe
|
||||
git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
Note that `npm dedupe` should always be followed by `npm install`, as shown above, even if
|
||||
`npm install` had already been executed before `npm dedupe`.
|
||||
|
||||
Optionally, these steps may be automated by installing the
|
||||
[npm-merge-driver](https://www.npmjs.com/package/npm-merge-driver):
|
||||
|
||||
```sh
|
||||
npx npm-merge-driver install -g
|
||||
```
|
||||
|
||||
## `fast-boot` and `npm link` - modifying the `node_modules` folder
|
||||
|
||||
During development or debugging, it is sometimes useful to temporarily modify the `node_modules`
|
||||
folder (with or without making the respective changes to the `npm-shrinkwrap.json` file),
|
||||
replacing dependencies with different versions. This can be achieved with the `npm link`
|
||||
command, or by manually editing or copying files to the `node_modules` folder.
|
||||
|
||||
Unexpected behavior may then be observed because of the CLI's use of the
|
||||
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
||||
`fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
|
||||
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
||||
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||
`node_modules` folder. In this situation:
|
||||
|
||||
* Manually delete the module cache file (typically `~/.balena/cli-module-cache.json`), or
|
||||
* Use the `bin/balena-dev` entry point (instead of `bin/balena`) as it does not activate
|
||||
`fast-boot2`.
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
[TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
|
||||
Typescript, in order to take advantage of static typing and formal programming interfaces.
|
||||
The migration towards Typescript is taking place gradually, as part of maintenance work or
|
||||
the implementation of new features.
|
||||
|
||||
Of historical interest, the CLI was originally written in [CoffeeScript](https://coffeescript.org)
|
||||
and used the [Capitano](https://github.com/balena-io/capitano) framework. All CoffeeScript code was
|
||||
migrated to either Javascript or Typescript, and Capitano was replaced with oclif. A few file or
|
||||
variable names still refer to this legacy, for example `automation/capitanodoc/capitanodoc.ts`.
|
||||
|
||||
## Programming style
|
||||
|
||||
`npm run build` also runs [balena-lint](https://www.npmjs.com/package/@balena/lint), which automatically
|
||||
reformats the code. Beyond that, we have a preference for Javascript promises over callbacks, and for
|
||||
`async/await` over `.then()`.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
One thing that most CLI bugs have in common is the absence of test cases exercising the broken
|
||||
code, so writing some test code is a great idea. Having said that, there are also some common
|
||||
gotchas to bear in mind:
|
||||
|
||||
* Forward slashes ('/') _vs._ backslashes ('\') in file paths. The Node.js
|
||||
[path.sep](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_sep) variable stores a
|
||||
platform-specific path separator character: the backslash on Windows and the forward slash on
|
||||
Linux and macOS. The
|
||||
[path.join](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_join_paths) function
|
||||
builds paths using such platform-specific path separator. However:
|
||||
* Note that Windows (kernel, cmd.exe, PowerShell, many applications) accepts ***both*** forward
|
||||
slashes and backslashes as path separators (including mixing them in a path string), so code
|
||||
like `mypath.split(path.sep)` may fail on Windows if `mypath` contains forward slashes. The
|
||||
[path.parse](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_parse_path) function
|
||||
understands both forward slashes and backslashes on Windows, and the
|
||||
[path.normalize](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_normalize_path)
|
||||
function will _replace_ forward slashes with backslashes.
|
||||
* In [tar](https://en.wikipedia.org/wiki/Tar_(computing)#File_format) streams sent to the Docker
|
||||
daemon and to balenaCloud, the forward slash is the only acceptable path separator, regardless
|
||||
of the OS where the CLI is running. Therefore, `path.sep` and `path.join` should never be used
|
||||
when handling paths in tar streams! `path.posix.join` may be used instead of `path.join`.
|
||||
|
||||
* Avoid using the system shell to execute external commands, for example:
|
||||
`child_process.exec('ssh "arg1" "arg2"');`
|
||||
`child_process.spawn('ssh "arg1" "arg2"', { shell: true });`
|
||||
Besides the usual security concerns of unsanitized strings, another problem is to get argument
|
||||
escaping right because of the differences between the Windows 'cmd.exe' shell and the Unix
|
||||
'/bin/sh'. For example, 'cmd.exe' doesn't recognize single quotes like '/bin/sh', and uses the
|
||||
caret (^) instead of the backslash as the escape character. Bug territory! Most of the time,
|
||||
it is possible to avoid relying on the shell altogether by providing a Javascript array of
|
||||
arguments:
|
||||
`spawn('ssh', ['arg1', 'arg2'], { shell: false});`
|
||||
To allow for logging and debugging, the [which](https://www.npmjs.com/package/which) package may
|
||||
be used to get the full path of a command before executing it, without relying on any shell:
|
||||
`const fullPath = await which('ssh');`
|
||||
`console.log(fullPath); # 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'`
|
||||
`spawn(fullPath, ['arg1', 'arg2'], { shell: false });`
|
||||
|
||||
* Avoid the `instanceof` operator when testing against classes/types from external packages
|
||||
(including base classes), because `npm install` may result in multiple versions of the same
|
||||
package being installed (to satisfy declared dependencies) and a false negative may result when
|
||||
comparing an object instance from one package version with a class of another package version
|
||||
(even if the implementations are identical in both packages). For example, once we fixed a bug
|
||||
where the test:
|
||||
`error instanceof BalenaApplicationNotFound`
|
||||
changed from true to false because `npm install` added an additional copy of the `balena-errors`
|
||||
package to satisfy a minor `balena-sdk` version update:
|
||||
`$ find node_modules -name balena-errors`
|
||||
`node_modules/balena-errors`
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
## Further debugging notes
|
||||
|
||||
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||
|
||||
replace: `spec: 'tests/**/*.spec.ts',`
|
||||
|
||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
169
INSTALL-ADVANCED.md
Normal file
169
INSTALL-ADVANCED.md
Normal file
@ -0,0 +1,169 @@
|
||||
# balena CLI Advanced Installation Options
|
||||
|
||||
**These are alternative, advanced installation options. Most users would prefer the [recommended,
|
||||
streamlined installation
|
||||
instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).**
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||
traditional graphical desktop application installers.
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||
in integrating the balena CLI in their existing projects or workflow.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
## Executable Installer
|
||||
|
||||
This is the recommended installation option on macOS and Windows. Follow the specific OS
|
||||
instructions:
|
||||
|
||||
* [Windows](./INSTALL-WINDOWS.md)
|
||||
* [macOS](./INSTALL-MAC.md)
|
||||
|
||||
> Note regarding WSL ([Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about))
|
||||
> If you would like to use WSL, follow the [installations instructions for
|
||||
> Linux](./INSTALL-LINUX.md) rather than Windows, as WSL consists of a Linux environment.
|
||||
|
||||
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-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> For these, consider the [NPM Installation](#npm-installation) option.
|
||||
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||
> folders and files also present in the `balena-cli` folder.
|
||||
|
||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
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 development tools to be installed first, as follows.
|
||||
|
||||
> **The balena CLI currently requires Node.js version 12 (min 12.8.0).**
|
||||
> **Versions 13 and later are not yet fully supported.**
|
||||
|
||||
### Install development tools
|
||||
|
||||
#### **Linux or WSL** (Windows Subsystem for Linux)
|
||||
|
||||
```sh
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script) to install
|
||||
Node.js, instead of using `apt-get`. Installing Node.js through `apt-get` is a common source of
|
||||
problems from permission errors to conflict with other system packages, and therefore not
|
||||
recommended.
|
||||
|
||||
#### **macOS**
|
||||
|
||||
* Download and install Apple's Command Line Tools from https://developer.apple.com/downloads/
|
||||
* Install Node.js through [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script):
|
||||
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 12
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
* `pacman -S git gcc make openssh p7zip`
|
||||
* [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-refreshed-wdk-for-windows-10-version-2004)
|
||||
* [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,
|
||||
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 --global --production windows-build-tools`
|
||||
|
||||
### Install the balena CLI
|
||||
|
||||
After installing the development tools, install the balena CLI with:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli --global --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
|
||||
container) in order to allow npm scripts like `postinstall` to be executed.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
* [Windows](./INSTALL-WINDOWS.md#additional-dependencies)
|
||||
* [macOS](./INSTALL-MAC.md#additional-dependencies)
|
||||
* [Linux](./INSTALL-LINUX.md#additional-dependencies)
|
||||
|
||||
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
|
||||
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images). Reasons why this
|
||||
may be desirable include:
|
||||
|
||||
* To avoid having to install Docker on the development machine / laptop.
|
||||
* To take advantage of a more powerful server (CPU, memory).
|
||||
* To build or run images "natively" on an ARM device, avoiding the need for QEMU emulation.
|
||||
|
||||
To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machine's IP address and
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. The `preload` command
|
||||
has additional requirements because the bind mount feature is used. For more details, see
|
||||
`balena help` for each command or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
85
INSTALL-LINUX.md
Normal file
85
INSTALL-LINUX.md
Normal file
@ -0,0 +1,85 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are suitable for most Linux distributions on Intel x86, such as
|
||||
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
|
||||
For the ARM architecture and for Linux distributions not based on glibc, such as
|
||||
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
|
||||
method.
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
1. Download the latest zip file from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest). Look for a file name that ends
|
||||
with "-standalone.zip", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
|
||||
The extracted contents will include a `balena-cli` folder.
|
||||
|
||||
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
|
||||
Check this [StackOverflow
|
||||
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
|
||||
for instructions. Close and reopen the terminal window so that the changes to `PATH`
|
||||
can take effect.
|
||||
|
||||
4. Check that the installation was successful by running the following commands on a
|
||||
terminal window:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
|
||||
To update the balena 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.
|
||||
|
||||
## sudo configuration
|
||||
|
||||
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
|
||||
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
|
||||
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
|
||||
the ***pre-existing*** `secure_path` setting, for example:
|
||||
|
||||
```text
|
||||
Defaults secure_path="/home/james/balena-cli:<pre-existing entries go here>"
|
||||
```
|
||||
|
||||
If an `/etc/sudoers` file does not exist, or if it does not contain a pre-existing
|
||||
`secure_path` setting, do not change it.
|
||||
|
||||
If you also have Docker installed, ensure that it can be executed ***without*** `sudo`, so that
|
||||
CLI commands like `balena build` and `balena preload` can also be executed without `sudo`.
|
||||
Check Docker's [post-installation
|
||||
steps](https://docs.docker.com/engine/install/linux-postinstall/) on how to achieve this.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build, deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Most Linux
|
||||
distributions will already have it installed. Otherwise, `sudo apt-get install openssh-client`
|
||||
should do the trick on Debian or Ubuntu.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like
|
||||
[Avahi](https://en.wikipedia.org/wiki/Avahi_(software)), which is installed by default on most
|
||||
desktop Linux distributions. Otherwise, on Debian or Ubuntu, the installation command would be
|
||||
`sudo apt-get install avahi-daemon`.
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used).
|
74
INSTALL-MAC.md
Normal file
74
INSTALL-MAC.md
Normal file
@ -0,0 +1,74 @@
|
||||
# balena CLI Installation Instructions for macOS
|
||||
|
||||
These instructions are for the recommended installation option. Advanced users may also be
|
||||
interested in [advanced installation options](./INSTALL-ADVANCED.md).
|
||||
|
||||
Selected operating system: **macOS**
|
||||
|
||||
1. Download the installer from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||
Look for a file name that ends with "-installer.pkg":
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful:
|
||||
- [Open the Terminal
|
||||
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
|
||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
in the next section.
|
||||
|
||||
To update the balena CLI, repeat the steps above for the new version.
|
||||
To uninstall it, run the following command on a terminal prompt:
|
||||
|
||||
```text
|
||||
sudo /usr/local/lib/balena-cli/bin/uninstall
|
||||
```
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. To check whether
|
||||
it is already installed, run `ssh` on a Terminal window. If it is not yet installed, the options
|
||||
include:
|
||||
|
||||
* Download the Xcode Command Line Tools from https://developer.apple.com/downloads
|
||||
* Or, if you have Xcode installed, open Xcode, choose Preferences → General → Downloads →
|
||||
Components → Command Line Tools → Install.
|
||||
* Or, install [Homebrew](https://brew.sh/), then `brew install openssh`
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)
|
||||
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
73
INSTALL-WINDOWS.md
Normal file
73
INSTALL-WINDOWS.md
Normal file
@ -0,0 +1,73 @@
|
||||
# balena CLI Installation Instructions for Windows
|
||||
|
||||
These instructions are for the recommended installation option. Advanced users may also be
|
||||
interested in [advanced installation options](./INSTALL-ADVANCED.md).
|
||||
|
||||
Selected operating system: **Windows**
|
||||
|
||||
1. Download the installer from the [latest release
|
||||
page](https://github.com/balena-io/balena-cli/releases/latest).
|
||||
Look for a file name that ends with "-installer.exe":
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful:
|
||||
- Click on the Windows Start Menu, type PowerShell, and then click
|
||||
on Windows PowerShell.
|
||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
described below.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
The `balena ssh` command requires the `ssh` command-line tool to be available. Microsoft started
|
||||
distributing an SSH client with Windows 10, which is automatically installed through Windows
|
||||
Update. To check whether it is installed, run `ssh` on a Windows Command Prompt or PowerShell. It
|
||||
can also be [manually
|
||||
installed](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)
|
||||
if needed. For older versions of Windows, there are several ssh/OpenSSH clients provided by 3rd
|
||||
parties.
|
||||
|
||||
The `balena ssh` command also requires an SSH key to be added to your balena account: see [SSH
|
||||
Access documentation](https://www.balena.io/docs/learn/manage/ssh-access/). The `balena key*`
|
||||
command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena scan
|
||||
|
||||
The `balena scan` command requires a multicast DNS (mDNS) service like Apple's Bonjour.
|
||||
Many Windows machines will already have this service installed, as it is bundled in popular
|
||||
applications such as Skype (Wikipedia lists [several others](https://en.wikipedia.org/wiki/Bonjour_(software))).
|
||||
Otherwise, Bonjour for Windows can be downloaded and installed from: https://support.apple.com/kb/DL999
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
117
INSTALL.md
117
INSTALL.md
@ -1,113 +1,12 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
The easiest and recommended way of installing the CLI on all platforms (Windows, Linux, macOS) is
|
||||
to use the [Standalone Installation](#standalone-installation) described below. Some specific CLI
|
||||
commands have a few extra installation steps: see section [Additional Dependencies](#additional-dependencies).
|
||||
Please select your operating system:
|
||||
|
||||
> **Windows users:** We now have a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg)
|
||||
for installing and getting started with the balena CLI on Windows!
|
||||
* [Windows](./INSTALL-WINDOWS.md)
|
||||
* [macOS](./INSTALL-MAC.md)
|
||||
* [Linux](./INSTALL-LINUX.md)
|
||||
|
||||
## Standalone Installation
|
||||
|
||||
1. Download the latest zip file for your OS from https://github.com/balena-io/balena-cli/releases.
|
||||
(Note that "[Darwin](https://en.wikipedia.org/wiki/Darwin_(operating_system))" is the
|
||||
appropriate zip file for macOS.)
|
||||
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:
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm) |
|
||||
[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)
|
||||
|
||||
Check that the installation was successful by opening or re-opening a command terminal window
|
||||
(so that the PATH environment variable changes take effect), and running these commands:
|
||||
|
||||
* `balena` - should print the balena CLI help
|
||||
* `balena version` - should print the installed CLI version
|
||||
|
||||
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 version 6 or above (**soon version 8 or above**)
|
||||
* Python 2.7
|
||||
* g++ compiler
|
||||
* make
|
||||
* git
|
||||
* Under Windows, the `windows-build-tools` npm package should be installed too, running the
|
||||
following command in an administrator console (available as 'Command Prompt (Admin)' when
|
||||
pressing Windows+X in Windows 7+) :
|
||||
`npm install -g --production windows-build-tools`
|
||||
|
||||
With those in place, the 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 sync` command (deprecated) currently requires `rsync` (>= 2.6.9) to be installed:
|
||||
* Linux: `apt-get install rsync`
|
||||
* macOS: [Xcode command-line tools](https://developer.apple.com/xcode/features/) or [homebrew](https://brew.sh/)
|
||||
* Windows: One option is to use the [MinGW](http://www.mingw.org) shell and install the
|
||||
`msys-rsync` package. Check the README file for other shell options under Windows.
|
||||
|
||||
## 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.
|
||||
> Note regarding WSL ([Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about))
|
||||
> If you would like to use WSL, follow the installations instructions for Linux
|
||||
> rather than Windows, as WSL consists of a Linux environment.
|
||||
|
186
README.md
186
README.md
@ -1,107 +1,179 @@
|
||||
# balena CLI
|
||||
|
||||
The official balena CLI tool.
|
||||
The official balena Command Line Interface.
|
||||
|
||||
[](http://badge.fury.io/js/balena-cli)
|
||||
[](https://david-dm.org/balena-io/balena-cli)
|
||||
[](https://gitter.im/balena-io/chat)
|
||||
|
||||
## About
|
||||
|
||||
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!
|
||||
The balena CLI is a Command Line Interface for [balenaCloud](https://www.balena.io/cloud/) or
|
||||
[openBalena](https://www.balena.io/open/). It is a software tool available for Windows, macOS and
|
||||
Linux, used through a command prompt / terminal window. It can be used interactively or invoked in
|
||||
scripts. The balena CLI builds on the [balena API](https://www.balena.io/docs/reference/api/overview/)
|
||||
and the [balena SDK](https://www.balena.io/docs/reference/sdk/node-sdk/), and can also be directly
|
||||
imported in Node.js applications. The balena CLI is an [open-source project on
|
||||
GitHub](https://github.com/balena-io/balena-cli/), and your contribution is also welcome!
|
||||
|
||||
## Installation
|
||||
|
||||
Check the [balena CLI installation instructions on GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
Check the [balena CLI installation instructions on
|
||||
GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Choosing a shell (command prompt/terminal)
|
||||
## Choosing a shell (command prompt/terminal)
|
||||
|
||||
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:
|
||||
are supported. Alternative shells include:
|
||||
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git gcc make openssh p7zip`
|
||||
* [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)
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
|
||||
actually uses MSYS2). For a screenshot, check this
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(a.k.a. Microsoft's "bash for Windows 10").
|
||||
* [Git for Windows](https://git-for-windows.github.io/).
|
||||
* [MinGW](http://www.mingw.org): install the `msys-rsync` and `msys-openssh` packages too.
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** should be selected. See
|
||||
[FAQ](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md) for using the
|
||||
balena CLI with WSL and Docker Desktop for Windows.
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
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)
|
||||
[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/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.
|
||||
|
||||
### Logging in
|
||||
## 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:
|
||||
new release to your fleet. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ balena login
|
||||
```
|
||||
|
||||
### Proxy support
|
||||
## Proxy support
|
||||
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
HTTP(S) proxies can be configured through any of the following methods, in precedence order
|
||||
(from higher to lower):
|
||||
|
||||
* Set the \`BALENARC_PROXY\` environment variable in URL format (with protocol, host, port, and
|
||||
optionally basic auth).
|
||||
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
|
||||
(project-specific or user-level) and set the \`proxy\` setting. It can be:
|
||||
* A string in URL format, or
|
||||
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
|
||||
* Alternatively, set the conventional \`https_proxy\` / \`HTTPS_PROXY\` / \`http_proxy\` / \`HTTP_PROXY\`
|
||||
environment variable (in the same standard URL format).
|
||||
* The `BALENARC_PROXY` environment variable in URL format, with protocol (`http` or `https`),
|
||||
host, port and optionally basic auth. Examples:
|
||||
* `export BALENARC_PROXY='https://bob:secret@proxy.company.com:12345'`
|
||||
* `export BALENARC_PROXY='http://localhost:8000'`
|
||||
|
||||
To get a proxy to work with the `balena ssh` command, check the
|
||||
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
* The `proxy` setting in the [CLI config
|
||||
file](https://www.npmjs.com/package/balena-settings-client#documentation). It may be:
|
||||
* A string in URL format, e.g. `proxy: 'http://localhost:8000'`
|
||||
* An object in the format:
|
||||
```yaml
|
||||
proxy:
|
||||
protocol: 'http'
|
||||
host: 'proxy.company.com'
|
||||
port: 12345
|
||||
proxyAuth: 'bob:secret'
|
||||
```
|
||||
|
||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||
`BALENARC_PROXY`.
|
||||
|
||||
### Proxy setup for balena ssh
|
||||
|
||||
In order to work behind a proxy server, the `balena ssh` command requires the
|
||||
[`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) to be installed.
|
||||
`proxytunnel` is available for Linux distributions like Ubuntu/Debian (`apt install proxytunnel`),
|
||||
and for macOS through [Homebrew](https://brew.sh/). Windows support is limited to the [Windows
|
||||
Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (e.g., by installing
|
||||
Ubuntu through the Microsoft App Store).
|
||||
|
||||
Ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
|
||||
SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
|
||||
server, it should be configured with the following rules in the `squid.conf` file:
|
||||
`acl SSL_ports port 22`
|
||||
`acl Safe_ports port 22`
|
||||
|
||||
### Proxy exclusion
|
||||
|
||||
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
|
||||
|
||||
> * This feature requires CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||
> Node.js version 10.16.0 or later.
|
||||
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
||||
|
||||
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
||||
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
|
||||
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
|
||||
default, because matching takes place before name resolution.
|
||||
|
||||
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
|
||||
BALENARC_NO_PROXY.
|
||||
|
||||
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
|
||||
that are matched against hostnames or IP addresses. For example:
|
||||
|
||||
```
|
||||
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
|
||||
```
|
||||
|
||||
Matched patterns are excluded from proxying. Wildcard expressions are documented at
|
||||
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
|
||||
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
|
||||
address like `192.168.1.2`.
|
||||
|
||||
## Command reference documentation
|
||||
|
||||
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`.
|
||||
) or by running `balena help --verbose`.
|
||||
|
||||
## Support, FAQ and troubleshooting
|
||||
|
||||
If you come across any problems or would like to get in touch:
|
||||
To learn more, troubleshoot issues, or to contact us for support:
|
||||
|
||||
* 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/).
|
||||
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
|
||||
* Ask us a question in the [balena forums](https://forums.balena.io/c/product-support)
|
||||
|
||||
## Contributing
|
||||
For CLI bug reports or feature requests, check the
|
||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
## Deprecation policy
|
||||
|
||||
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we have decided to
|
||||
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
|
||||
typing and formal programming interfaces. The migration is taking place gradually, as part of
|
||||
maintenance work or the implementation of new features.
|
||||
The balena CLI uses [semver versioning](https://semver.org/), with the concepts
|
||||
of major, minor and patch version releases.
|
||||
|
||||
After cloning this repository and running `npm install` you can build the CLI using `npm run build`.
|
||||
You can then run the generated build using `./bin/balena`.
|
||||
In order to ease development:
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
* you can build the CLI using the `npm run build:fast` variant which skips some of the build steps or
|
||||
* you can use `./bin/balena-dev` which live transpiles the sources of the CLI.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
In either case, before opening a PR be sure to also test your changes with `npm test`.
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
## Note on editing this README document
|
||||
|
||||
This file is edited/created by hand, but it is then automatically parsed to extract selected
|
||||
subsections for the [CLI's web documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
The code that parses this file is in [`automation/capitanodoc/capitanodoc.ts`
|
||||
](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
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!
|
||||
|
||||
## License
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
# FAQ & Troubleshooting
|
||||
# balena CLI FAQ & Troubleshooting
|
||||
|
||||
This document contains some common issues, questions and answers related to the balena CLI.
|
||||
|
||||
## Where is my configuration file?
|
||||
## Where is the balena CLI's configuration file located?
|
||||
|
||||
The per-user configuration file lives in `$HOME/.balenarc.yml` or `%UserProfile%\_balenarc.yml`, in
|
||||
Unix based operating systems and Windows respectively.
|
||||
@ -10,53 +8,48 @@ 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?
|
||||
## How do I point the balena CLI to the staging environment?
|
||||
|
||||
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.
|
||||
Set the `BALENARC_BALENA_URL=balena-staging.com` environment variable, or add
|
||||
`balenaUrl: balena-staging.com` to the balena CLI's configuration file.
|
||||
|
||||
## 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`.
|
||||
The balena CLI persists the session token, as well as cached assets, to `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`. This directory can be changed by setting an environment variable,
|
||||
`BALENARC_DATA_DIRECTORY=/opt/balena`, or by adding `dataDirectory: /opt/balena` to the CLI's
|
||||
configuration file, replacing `/opt/balena` with the desired directory.
|
||||
|
||||
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.
|
||||
## After burning to an SD card, my device doesn't boot
|
||||
|
||||
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.
|
||||
Check whether the downloaded image is incomplete (download was interrupted) or corrupted.
|
||||
|
||||
## After burning to an sdcard, my device doesn't boot
|
||||
Try clearing the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and running the
|
||||
command again.
|
||||
|
||||
- The downloaded image is not complete (download was interrupted).
|
||||
## I get a permission error when burning to an SD card
|
||||
|
||||
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.
|
||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||
|
||||
## I get a permission error when burning to an sdcard
|
||||
## I get `connect ETIMEDOUT` with `balena tunnel`
|
||||
|
||||
- The SDCard is locked.
|
||||
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
||||
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
||||
|
||||
### I get EINVAL errors on Cygwin
|
||||
## I get EINVAL errors on Cygwin
|
||||
|
||||
The errors look something like this:
|
||||
The errors may look something like this:
|
||||
|
||||
```
|
||||
net.js:156
|
||||
this._handle.open(options.fd);
|
||||
^
|
||||
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\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).
|
||||
Some interactive widgets don't work on `Cygwin`. On Windows, PowerShell or `cmd.exe` are better
|
||||
supported. Alternative shells are [listed in the README
|
||||
file](./README.md#choosing-a-shell-command-promptterminal).
|
||||
|
||||
## I get `Invalid MBR boot signature` when configuring a device
|
||||
|
||||
@ -76,10 +69,63 @@ Or in Windows:
|
||||
|
||||
## I get `EACCES: permission denied` when logging in
|
||||
|
||||
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.
|
||||
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 the CLI was executed as the `root` user.
|
||||
|
||||
Try resetting the ownership by running:
|
||||
|
||||
```sh
|
||||
$ 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 on the remote machine), 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 simply 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" will, by default, attempt to reach the
|
||||
Docker daemon 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
|
||||
|
34
appveyor.yml
34
appveyor.yml
@ -1,34 +0,0 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
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: 6
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- npm install -g npm@4
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm install
|
||||
|
||||
build: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- cmd: npm test
|
||||
|
||||
deploy_script:
|
||||
- IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
- IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
540
automation/build-bin.ts
Executable file → Normal file
540
automation/build-bin.ts
Executable file → Normal file
@ -14,50 +14,532 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { JsonVersions } from '../lib/commands/version';
|
||||
|
||||
import { run as oclifRun } from 'oclif';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as klaw from 'klaw';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { exec as execPkg } from 'pkg';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
loadPackageJson,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
console.log('Building package...\n');
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
execPkg(['--target', 'host', '--output', 'build-bin/balena', 'package.json'])
|
||||
.then(() => {
|
||||
const xpaths: Array<[string, string[]]> = [
|
||||
// [platform, [path, to, file]]
|
||||
['*', ['opn', 'xdg-open']],
|
||||
['darwin', ['denymount', 'bin', 'denymount']],
|
||||
];
|
||||
return Bluebird.map(xpaths, ([platform, xpath]) => {
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
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('win32', `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']],
|
||||
};
|
||||
|
||||
/**
|
||||
* Given the output of `pkg` as a string (containing warning messages),
|
||||
* diff it against previously saved output of known "safe" warnings.
|
||||
* Throw an error if the diff is not empty.
|
||||
*/
|
||||
async function diffPkgOutput(pkgOut: string) {
|
||||
const { monochrome } = await import('../tests/helpers');
|
||||
const relSavedPath = path.join(
|
||||
'tests',
|
||||
'test-data',
|
||||
'pkg',
|
||||
`expected-warnings-${process.platform}.txt`,
|
||||
);
|
||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||
const ignoreStartsWith = [
|
||||
'> pkg@',
|
||||
'> Fetching base Node.js binaries',
|
||||
' fetched-',
|
||||
'prebuild-install WARN install No prebuilt binaries found',
|
||||
];
|
||||
const modulesRE =
|
||||
process.platform === 'win32'
|
||||
? /(?<=[ '])([A-Z]:)?\\.+?\\node_modules(?=\\)/
|
||||
: /(?<=[ '])\/.+?\/node_modules(?=\/)/;
|
||||
const buildRE =
|
||||
process.platform === 'win32'
|
||||
? /(?<=[ '])([A-Z]:)?\\.+\\build(?=\\)/
|
||||
: /(?<=[ '])\/.+\/build(?=\/)/;
|
||||
|
||||
const cleanLines = (chunks: string | string[]) => {
|
||||
const lines = typeof chunks === 'string' ? chunks.split('\n') : chunks;
|
||||
return lines
|
||||
.map((line: string) => monochrome(line)) // remove ASCII colors
|
||||
.filter((line: string) => !/^\s*$/.test(line)) // blank lines
|
||||
.filter((line: string) =>
|
||||
ignoreStartsWith.every((i) => !line.startsWith(i)),
|
||||
)
|
||||
.map((line: string) => {
|
||||
// replace absolute paths with relative paths
|
||||
let replaced = line.replace(modulesRE, 'node_modules');
|
||||
if (replaced === line) {
|
||||
replaced = line.replace(buildRE, 'build');
|
||||
}
|
||||
return replaced;
|
||||
});
|
||||
};
|
||||
|
||||
pkgOut = cleanLines(pkgOut).join('\n');
|
||||
const { readFile } = (await import('fs')).promises;
|
||||
const expectedOut = cleanLines(await readFile(absSavedPath, 'utf8')).join(
|
||||
'\n',
|
||||
);
|
||||
if (expectedOut !== pkgOut) {
|
||||
const sep =
|
||||
'================================================================================';
|
||||
const diff = diffLines(expectedOut, pkgOut);
|
||||
const msg = `pkg output does not match expected output from "${relSavedPath}"
|
||||
Diff:
|
||||
${sep}
|
||||
${diff}
|
||||
${sep}
|
||||
Check whether the new or changed pkg warnings are safe to ignore, then update
|
||||
"${relSavedPath}"
|
||||
and share the result of your investigation as comments on the pull request.
|
||||
Hint: the fix is often a matter of updating the 'pkg.scripts' or 'pkg.assets'
|
||||
sections in the CLI's 'package.json' file, or a matter of updating the
|
||||
'buildPkg' function in 'automation/build-bin.ts'. Sometimes it requires
|
||||
patching dependencies: See for example 'patches/all/open+7.0.2.patch'.
|
||||
${sep}
|
||||
`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `pkg.exec` to generate the standalone zip file, capturing its warning
|
||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
||||
*/
|
||||
async function execPkg(...args: any[]) {
|
||||
const { exec: pkgExec } = await import('pkg');
|
||||
const outTap = new StdOutTap(true);
|
||||
try {
|
||||
outTap.tap();
|
||||
await (pkgExec as any)(...args);
|
||||
} catch (err) {
|
||||
outTap.untap();
|
||||
console.log(outTap.stdoutBuf.join(''));
|
||||
console.error(outTap.stderrBuf.join(''));
|
||||
throw err;
|
||||
}
|
||||
outTap.untap();
|
||||
await diffPkgOutput(outTap.allBuf.join(''));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 paths: Array<[string, string[], string[]]> = [
|
||||
// [platform, [source path], [destination path]]
|
||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||
];
|
||||
await Promise.all(
|
||||
paths.map(([platform, source, dest]) => {
|
||||
if (platform === '*' || platform === process.platform) {
|
||||
// eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
|
||||
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
|
||||
return fs.copy(
|
||||
path.join(ROOT, 'node_modules', ...xpath),
|
||||
path.join(ROOT, 'build-bin', xpath.pop()!),
|
||||
path.join(ROOT, 'node_modules', ...source),
|
||||
path.join(ROOT, 'build-bin', ...dest),
|
||||
);
|
||||
}
|
||||
}).return();
|
||||
})
|
||||
.then(() => {
|
||||
return filehound
|
||||
.create()
|
||||
.paths(path.join(ROOT, 'node_modules'))
|
||||
.ext(['node', 'dll'])
|
||||
.find();
|
||||
})
|
||||
.then(nativeExtensions => {
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensions.join('\n')}`);
|
||||
}),
|
||||
);
|
||||
const nativeExtensionPaths: string[] = await filehound
|
||||
.create()
|
||||
.paths(path.join(ROOT, 'node_modules'))
|
||||
.ext(['node', 'dll'])
|
||||
.find();
|
||||
|
||||
return nativeExtensions.map(extPath => {
|
||||
return fs.copy(
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
||||
|
||||
await Promise.all(
|
||||
nativeExtensionPaths.map((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() {
|
||||
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)
|
||||
let { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||
'version',
|
||||
'-j',
|
||||
]);
|
||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: stderr.split(/\r?\n/),
|
||||
out: stdout.split(/\r?\n/),
|
||||
});
|
||||
stdout = filtered.out.join('\n');
|
||||
stderr = filtered.err.join('\n');
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
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}"`,
|
||||
);
|
||||
}
|
||||
if (filtered.err.length > 0) {
|
||||
const err = filtered.err.join('\n');
|
||||
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function signFilesForNotarization() {
|
||||
console.log('Deleting unneeded zip files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node.bak')) {
|
||||
console.log('Removing pkg .node.bak file', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
if (
|
||||
path.basename(item.path).endsWith('.zip') &&
|
||||
path.dirname(item.path).includes('test')
|
||||
) {
|
||||
console.log('Removing zip', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
// Sign all .node files first
|
||||
console.log('Signing .node files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', async (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node')) {
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log('Signing other binaries...');
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
await buildPkg();
|
||||
await testPkg();
|
||||
await zipPkg();
|
||||
console.log(`Standalone zip package build completed`);
|
||||
} catch (error) {
|
||||
console.error(`Error creating or testing standalone zip package`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Apple Installer Notarization to continue
|
||||
*/
|
||||
async function notarizeMacInstaller(): Promise<void> {
|
||||
const appleId = 'accounts+apple@balena.io';
|
||||
const { notarize } = await import('electron-notarize');
|
||||
await notarize({
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword: '@keychain:CLI_PASSWORD',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif 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));
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
console.log('Signing files for notarization...');
|
||||
await signFilesForNotarization();
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif "${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();
|
||||
} else if (process.platform === 'darwin') {
|
||||
console.log('Notarizing package...');
|
||||
await notarizeMacInstaller(); // Notarize
|
||||
console.log('Package notarized.');
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the npm `catch-uncommitted` package in order to run it
|
||||
* conditionally, only when:
|
||||
* - A CI env var is set (CI=true), and
|
||||
* - The OS is not Windows. (`catch-uncommitted` fails on Windows)
|
||||
*/
|
||||
export async function catchUncommitted(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] CI=${process.env.CI} platform=${process.platform}`);
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
['true', 'yes', '1'].includes(process.env.CI.toLowerCase()) &&
|
||||
process.platform !== 'win32'
|
||||
) {
|
||||
await whichSpawn('npx', [
|
||||
'catch-uncommitted',
|
||||
'--catch-no-git',
|
||||
'--skip-node-versionbot-changes',
|
||||
'--ignore-space-at-eol',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function testShrinkwrap(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] platform=${process.platform}`);
|
||||
}
|
||||
if (process.platform !== 'win32') {
|
||||
await whichSpawn(path.resolve(__dirname, 'test-lock-deduplicated.sh'));
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { MarkdownFileParser } from './utils';
|
||||
|
||||
/**
|
||||
@ -27,96 +26,163 @@ import { MarkdownFileParser } from './utils';
|
||||
* some content to this object.
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'Balena CLI Documentation',
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/actions/api-key.js'],
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: ['build/actions/app.js'],
|
||||
title: 'Fleet',
|
||||
files: [
|
||||
'build/commands/fleets.js',
|
||||
'build/commands/fleet/index.js',
|
||||
'build/commands/fleet/create.js',
|
||||
'build/commands/fleet/purge.js',
|
||||
'build/commands/fleet/rename.js',
|
||||
'build/commands/fleet/restart.js',
|
||||
'build/commands/fleet/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: ['build/actions/auth.js'],
|
||||
files: [
|
||||
'build/commands/login.js',
|
||||
'build/commands/logout.js',
|
||||
'build/commands/whoami.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: ['build/actions/device.js'],
|
||||
files: [
|
||||
'build/commands/devices/index.js',
|
||||
'build/commands/devices/supported.js',
|
||||
'build/commands/device/index.js',
|
||||
'build/commands/device/deactivate.js',
|
||||
'build/commands/device/identify.js',
|
||||
'build/commands/device/init.js',
|
||||
'build/commands/device/local-mode.js',
|
||||
'build/commands/device/move.js',
|
||||
'build/commands/device/os-update.js',
|
||||
'build/commands/device/public-url.js',
|
||||
'build/commands/device/purge.js',
|
||||
'build/commands/device/reboot.js',
|
||||
'build/commands/device/register.js',
|
||||
'build/commands/device/rename.js',
|
||||
'build/commands/device/restart.js',
|
||||
'build/commands/device/rm.js',
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Releases',
|
||||
files: [
|
||||
'build/commands/releases.js',
|
||||
'build/commands/release/index.js',
|
||||
'build/commands/release/finalize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: ['build/actions/environment-variables.js'],
|
||||
files: [
|
||||
'build/commands/envs.js',
|
||||
'build/commands/env/add.js',
|
||||
'build/commands/env/rename.js',
|
||||
'build/commands/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: ['build/actions/tags.js'],
|
||||
files: [
|
||||
'build/commands/tags.js',
|
||||
'build/commands/tag/rm.js',
|
||||
'build/commands/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
files: ['build/actions/help.js'],
|
||||
},
|
||||
{
|
||||
title: 'Information',
|
||||
files: ['build/actions/info.js'],
|
||||
title: 'Help and Version',
|
||||
files: ['help', 'build/commands/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: ['build/actions/keys.js'],
|
||||
files: [
|
||||
'build/commands/keys.js',
|
||||
'build/commands/key/index.js',
|
||||
'build/commands/key/add.js',
|
||||
'build/commands/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/actions/logs.js'],
|
||||
files: ['build/commands/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Sync',
|
||||
files: ['build/actions/sync.js'],
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
files: ['build/actions/ssh.js', 'build/actions/tunnel.js'],
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/commands/scan.js',
|
||||
'build/commands/ssh.js',
|
||||
'build/commands/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions/notes.js'],
|
||||
files: ['build/commands/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: ['build/actions/os.js'],
|
||||
files: [
|
||||
'build/commands/os/build-config.js',
|
||||
'build/commands/os/configure.js',
|
||||
'build/commands/os/versions.js',
|
||||
'build/commands/os/download.js',
|
||||
'build/commands/os/initialize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: ['build/actions/config.js'],
|
||||
files: [
|
||||
'build/commands/config/generate.js',
|
||||
'build/commands/config/inject.js',
|
||||
'build/commands/config/read.js',
|
||||
'build/commands/config/reconfigure.js',
|
||||
'build/commands/config/write.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/actions/preload.js'],
|
||||
files: ['build/commands/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/actions/push.js'],
|
||||
files: ['build/commands/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions/settings.js'],
|
||||
files: ['build/commands/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: ['build/actions/local/index.js'],
|
||||
files: [
|
||||
'build/commands/local/configure.js',
|
||||
'build/commands/local/flash.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/actions/build.js', 'build/actions/deploy.js'],
|
||||
files: ['build/commands/build.js', 'build/commands/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/actions/join.js', 'build/actions/leave.js'],
|
||||
files: ['build/commands/join.js', 'build/commands/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/actions/util.js'],
|
||||
files: ['build/commands/util/available-drives.js'],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
files: ['build/commands/support.js'],
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -143,8 +209,11 @@ export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
return match && match[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Getting Started'),
|
||||
mdParser.getSectionOfTitle('Choosing a shell (command prompt/terminal)'),
|
||||
mdParser.getSectionOfTitle('Logging in'),
|
||||
mdParser.getSectionOfTitle('Proxy support'),
|
||||
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
|
||||
mdParser.getSectionOfTitle('Deprecation policy'),
|
||||
]);
|
||||
capitanoDoc.introduction = sections.join('\n');
|
||||
return capitanoDoc;
|
||||
|
24
automation/capitanodoc/doc-types.d.ts
vendored
24
automation/capitanodoc/doc-types.d.ts
vendored
@ -1,4 +1,22 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
/**
|
||||
* @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';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
export interface Document {
|
||||
title: string;
|
||||
@ -8,7 +26,7 @@ export interface Document {
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: CommandDefinition[];
|
||||
commands: OclifCommand[];
|
||||
}
|
||||
|
||||
export { CommandDefinition as Command };
|
||||
export { OclifCommand };
|
||||
|
@ -14,12 +14,11 @@
|
||||
* 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 { Category, Document } from './doc-types';
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
import { stripIndent } from '../../lib/utils/lazy';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
@ -39,25 +38,60 @@ export async function renderMarkdown(): Promise<string> {
|
||||
commands: [],
|
||||
};
|
||||
|
||||
for (const file of commandCategory.files) {
|
||||
const actions: any = require(path.join(process.cwd(), file));
|
||||
|
||||
if (actions.signature) {
|
||||
category.commands.push(_.omit(actions, 'action'));
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
category.commands.push(_.omit(actionCommand, 'action'));
|
||||
}
|
||||
}
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(...importOclifCommands(jsFilename));
|
||||
}
|
||||
|
||||
result.categories.push(category);
|
||||
}
|
||||
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
// Help is now managed via a plugin
|
||||
// This fake command allows capitanodoc to include help in docs
|
||||
class FakeHelpCommand {
|
||||
description = stripIndent`
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
|
||||
List balena commands, or get detailed help for a specific command.
|
||||
`;
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help login',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
args = [
|
||||
{
|
||||
name: 'command',
|
||||
description: 'command to show help for',
|
||||
},
|
||||
];
|
||||
|
||||
usage = 'help [command]';
|
||||
|
||||
flags = {
|
||||
verbose: {
|
||||
description: 'show additional commands',
|
||||
char: '-v',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||
// an error when parsed. This should be improved so that `usage` does not have
|
||||
// to be overridden if not necessary.
|
||||
|
||||
const command: OclifCommand =
|
||||
jsFilename === 'help'
|
||||
? (new FakeHelpCommand() as unknown as 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.
|
||||
@ -67,8 +101,9 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
printMarkdown();
|
||||
|
@ -14,81 +14,115 @@
|
||||
* 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 { Category, Command, Document } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||
import { Category, Document, OclifCommand } from './doc-types';
|
||||
|
||||
export function renderCommand(command: Command) {
|
||||
let result = `## ${ent.encode(command.signature)}\n\n${command.help}\n`;
|
||||
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.options)) {
|
||||
result += '\n### Options';
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map((v) => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
for (const option of command.options!) {
|
||||
result += `\n\n#### ${utils.parseSignature(option)}\n\n${
|
||||
option.description
|
||||
}`;
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function renderCategory(category: Category) {
|
||||
let result = `# ${category.title}\n`;
|
||||
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result += `\n${renderCommand(command)}`;
|
||||
result.push(...renderOclifCommand(command));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAnchor(command: Command) {
|
||||
return (
|
||||
'#' +
|
||||
command.signature
|
||||
.replace(/\s/g, '-')
|
||||
.replace(/</g, '-')
|
||||
.replace(/>/g, '-')
|
||||
.replace(/\[/g, '-')
|
||||
.replace(/\]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/-$/, '')
|
||||
.replace(/\.\.\./g, '')
|
||||
.replace(/\|/g, '')
|
||||
.toLowerCase()
|
||||
);
|
||||
function getAnchor(cmdSignature: string): string {
|
||||
return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
|
||||
}
|
||||
|
||||
export function renderToc(categories: Category[]) {
|
||||
let result = `# CLI Command Reference\n`;
|
||||
function renderToc(categories: Category[]): string[] {
|
||||
const result = [`# CLI Command Reference`];
|
||||
|
||||
for (const category of categories) {
|
||||
result += `\n- ${category.title}\n\n`;
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map((command) => {
|
||||
const signature = capitanoizeOclifUsage(command.usage);
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const command of category.commands) {
|
||||
result += `\t- [${ent.encode(command.signature)}](${getAnchor(
|
||||
command,
|
||||
)})\n`;
|
||||
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<OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: OclifCommand, x: string) =>
|
||||
(cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
let result = `# ${doc.title}\n\n${doc.introduction}\n\n${renderToc(
|
||||
doc.categories,
|
||||
)}`;
|
||||
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
...renderToc(doc.categories),
|
||||
];
|
||||
for (const category of doc.categories) {
|
||||
result += `\n${renderCategory(category)}`;
|
||||
result.push(...renderCategory(category));
|
||||
}
|
||||
|
||||
return result;
|
||||
return result.join('\n\n');
|
||||
}
|
||||
|
@ -14,10 +14,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { OptionDefinition } from 'capitano';
|
||||
|
||||
import type { 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) {
|
||||
@ -32,14 +32,14 @@ export function getOptionSignature(signature: string) {
|
||||
return `${getOptionPrefix(signature)}${signature}`;
|
||||
}
|
||||
|
||||
export function parseSignature(option: OptionDefinition) {
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (_.isArray(option.alias)) {
|
||||
if (Array.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (_.isString(option.alias)) {
|
||||
} else if (typeof option.alias === 'string') {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
@ -93,7 +93,7 @@ export class MarkdownFileParser {
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
rl.on('line', line => {
|
||||
rl.on('line', (line) => {
|
||||
// try to match a line like "## Getting Started", where the number
|
||||
// of '#' characters is the sectionLevel ('##' -> 2), and the
|
||||
// sectionTitle is "Getting Started"
|
||||
@ -126,9 +126,7 @@ export class MarkdownFileParser {
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${
|
||||
this.mdFilePath
|
||||
}"`,
|
||||
`Markdown section not found: title="${title}" file="${this.mdFilePath}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
84
automation/check-doc.js
Normal file
84
automation/check-doc.js
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const stripIndent = require('common-tags/lib/stripIndent');
|
||||
const _ = require('lodash');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
/**
|
||||
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
|
||||
* issuing an error if balena-cli.md is older.
|
||||
* If balena-cli.md does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
]);
|
||||
const stagedFiles = _.uniq([
|
||||
...gitStatus.created,
|
||||
...gitStatus.staged,
|
||||
...gitStatus.renamed.map((o) => o.to),
|
||||
])
|
||||
// select only staged files that start with lib/ or typings/
|
||||
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
||||
.map((f) => path.join(ROOT, f));
|
||||
|
||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||
fStats.forEach((fStat, index) => {
|
||||
if (fStat.mtimeMs > docStat.mtimeMs) {
|
||||
const fPath = stagedFiles[index];
|
||||
throw new Error(stripIndent`
|
||||
--------------------------------------------------------------------------------
|
||||
ERROR: at least one staged file: "${fPath}"
|
||||
has a more recent modification timestamp than the documentation file:
|
||||
"${docFile}"
|
||||
|
||||
This probably means that \`npm run build\` or \`npm test\` have not been executed,
|
||||
and this error can be fixed by doing so. Running \`npm run build\` or \`npm test\`
|
||||
before commiting is required in order to update the CLI markdown documentation
|
||||
(in case any command-line options were updated, added or removed) and also to
|
||||
catch Typescript type check errors sooner and reduce overall waiting time, given
|
||||
that the CI build/tests are currently rather lengthy.
|
||||
|
||||
If you need/wish to bypass this check without running \`npm run build\`, run:
|
||||
npx touch -am "${docFile}"
|
||||
and then try again.
|
||||
--------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await checkBuildTimestamps();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
71
automation/check-npm-version.js
Normal file
71
automation/check-npm-version.js
Normal file
@ -0,0 +1,71 @@
|
||||
#!/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.
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
if (match == null) {
|
||||
throw new Error(`Invalid semver version: ${version}`);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} v1
|
||||
* @param {string} v2
|
||||
*/
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
} else if (v1Array[i] > v2Array[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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.)
|
||||
throw new Error(`\
|
||||
-----------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-----------------------------------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
try {
|
||||
checkNpmVersion();
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
38
automation/custom-types.d.ts
vendored
38
automation/custom-types.d.ts
vendored
@ -1,38 +0,0 @@
|
||||
declare module 'pkg' {
|
||||
export function exec(args: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
declare module 'filehound' {
|
||||
export function create(): FileHound;
|
||||
|
||||
export interface FileHound {
|
||||
paths(paths: string[]): FileHound;
|
||||
paths(...paths: string[]): FileHound;
|
||||
ext(extensions: string[]): FileHound;
|
||||
ext(...extensions: string[]): FileHound;
|
||||
find(): Promise<string[]>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'publish-release' {
|
||||
interface PublishOptions {
|
||||
token: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
tag: string;
|
||||
name: string;
|
||||
reuseRelease?: boolean;
|
||||
assets: string[];
|
||||
}
|
||||
|
||||
interface Release {
|
||||
html_url: string;
|
||||
}
|
||||
|
||||
let publishRelease: (
|
||||
args: PublishOptions,
|
||||
callback: (e: Error, release: Release) => void,
|
||||
) => void;
|
||||
|
||||
export = publishRelease;
|
||||
}
|
@ -14,71 +14,244 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as archiver from 'archiver';
|
||||
import * as Promise from 'bluebird';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as mkdirp from 'mkdirp';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as publishRelease from 'publish-release';
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const publishReleaseAsync = Promise.promisify(publishRelease);
|
||||
const mkdirpAsync = Promise.promisify<string | null, string>(mkdirp);
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
const version = 'v' + packageJSON.version;
|
||||
const outputFile = path.join(
|
||||
ROOT,
|
||||
'build-zip',
|
||||
`balena-cli-${version}-${os.platform()}-${os.arch()}.zip`,
|
||||
);
|
||||
|
||||
mkdirpAsync(path.dirname(outputFile))
|
||||
.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) => {
|
||||
console.log('Zipping build...');
|
||||
|
||||
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();
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
console.log('Build zipped');
|
||||
console.log('Publishing build...');
|
||||
|
||||
return publishReleaseAsync({
|
||||
/**
|
||||
* 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: [outputFile],
|
||||
});
|
||||
})
|
||||
.then(release => {
|
||||
console.log(`Release ${version} successful: ${release.html_url}`);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
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) {
|
||||
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (
|
||||
require('@octokit/rest') as typeof import('@octokit/rest')
|
||||
).Octokit.plugin(
|
||||
(
|
||||
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
|
||||
).throttling,
|
||||
);
|
||||
return 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') as typeof import('parse-link-header');
|
||||
const parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${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;
|
||||
type Release =
|
||||
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||
for await (const response of octokit.paginate.iterator<Release>(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,
|
||||
);
|
||||
}
|
||||
|
107
automation/run.ts
Normal file
107
automation/run.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @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,
|
||||
catchUncommitted,
|
||||
testShrinkwrap,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
process.env.DEBUG?.toLowerCase(),
|
||||
)
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
async function parse(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
|
||||
console.error(`[debug] automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
throw new Error('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
'test-shrinkwrap': testShrinkwrap,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
throw new Error(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = (await import('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 {
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
if (typeof err === 'object') {
|
||||
err.message = `"${arg}": ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See jsdoc for parse() function above */
|
||||
export async function run(args?: string[]) {
|
||||
try {
|
||||
await parse(args);
|
||||
} catch (e) {
|
||||
console.error(e.message ? `Error: ${e.message}` : e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
run();
|
20
automation/test-lock-deduplicated.sh
Executable file
20
automation/test-lock-deduplicated.sh
Executable file
@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cp npm-shrinkwrap.json npm-shrinkwrap.json.old
|
||||
npm i
|
||||
npm dedupe
|
||||
npm i
|
||||
|
||||
if ! diff -q npm-shrinkwrap.json npm-shrinkwrap.json.old > /dev/null; then
|
||||
rm npm-shrinkwrap.json.old
|
||||
echo "** npm-shrinkwrap.json was not deduplicated or not fully committed - FAIL **";
|
||||
echo "** This can usually be fixed with: **";
|
||||
echo "** git checkout master -- npm-shrinkwrap.json **";
|
||||
echo "** rm -rf node_modules **";
|
||||
echo "** npm install && npm dedupe && npm install **";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
rm npm-shrinkwrap.json.old
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts",
|
||||
"../typings/*.d.ts"
|
||||
]
|
||||
}
|
140
automation/update-module.ts
Normal file
140
automation/update-module.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||
|
||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
maybeChangeType = maybeChangeType.toLowerCase();
|
||||
switch (maybeChangeType) {
|
||||
case 'patch':
|
||||
case 'minor':
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
throw new Error(`Invalid change type: '${maybeChangeType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
const compareSemverChangeType = (oldVersion: string, newVersion: string) => {
|
||||
const oldSemver = semver.parse(oldVersion)!;
|
||||
const newSemver = semver.parse(newVersion)!;
|
||||
|
||||
for (const changeType of changeTypes) {
|
||||
if (oldSemver[changeType] !== newSemver[changeType]) {
|
||||
return changeType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (cmd: string) => {
|
||||
console.info(`Running '${cmd}'`);
|
||||
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout?.pipe(process.stdout);
|
||||
p.stderr?.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
const getVersion = async (module: string): Promise<string> => {
|
||||
const { stdout } = await run(`npm ls --json --depth 0 ${module}`);
|
||||
return JSON.parse(stdout).dependencies[module].version;
|
||||
};
|
||||
|
||||
interface Upstream {
|
||||
repo: string;
|
||||
url: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
const getUpstreams = async () => {
|
||||
const fs = await import('fs');
|
||||
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
|
||||
|
||||
const yaml = await import('js-yaml');
|
||||
const { upstream } = yaml.load(repoYaml) as {
|
||||
upstream: Upstream[];
|
||||
};
|
||||
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`;
|
||||
|
||||
async function $main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
throw new Error(getUsage(upstreams, '$upstreamName'));
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
throw new Error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
throw new Error(getUsage(upstreams, upstreamName));
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
|
||||
const oldVersion = await getVersion(packageName);
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
throw new Error(`Already on version '${newVersion}'`);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
const semverChangeType = compareSemverChangeType(oldVersion, newVersion);
|
||||
|
||||
const changeType = process.argv[4]
|
||||
? // if the caller specified a change type, use that one
|
||||
validateChangeType(process.argv[4])
|
||||
: // use the same change type as in the dependency, but avoid major bumps
|
||||
semverChangeType && semverChangeType !== 'major'
|
||||
? semverChangeType
|
||||
: 'minor';
|
||||
console.log(`Using Change-type: ${changeType}`);
|
||||
|
||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||
currentBranch = currentBranch.trim();
|
||||
console.log(`Currenty on branch: '${currentBranch}'`);
|
||||
if (currentBranch === 'master') {
|
||||
await run(`git checkout -b "update-${upstreamName}-${newVersion}"`);
|
||||
}
|
||||
|
||||
await run(`git add package.json npm-shrinkwrap.json`);
|
||||
await run(
|
||||
`git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
main();
|
148
automation/utils.ts
Normal file
148
automation/utils.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
export class StdOutTap {
|
||||
public stdoutBuf: string[] = [];
|
||||
public stderrBuf: string[] = [];
|
||||
public allBuf: string[] = []; // both stdout and stderr
|
||||
|
||||
protected origStdoutWrite: typeof process.stdout.write;
|
||||
protected origStderrWrite: typeof process.stdout.write;
|
||||
|
||||
constructor(protected printDots = false) {}
|
||||
|
||||
tap() {
|
||||
this.origStdoutWrite = process.stdout.write;
|
||||
this.origStderrWrite = process.stderr.write;
|
||||
|
||||
process.stdout.write = (chunk: string, ...args: any[]): boolean => {
|
||||
this.stdoutBuf.push(chunk);
|
||||
this.allBuf.push(chunk);
|
||||
const str = this.printDots ? '.' : chunk;
|
||||
return this.origStdoutWrite.call(process.stdout, str, ...args);
|
||||
};
|
||||
|
||||
process.stderr.write = (chunk: string, ...args: any[]): boolean => {
|
||||
this.stderrBuf.push(chunk);
|
||||
this.allBuf.push(chunk);
|
||||
const str = this.printDots ? '.' : chunk;
|
||||
return this.origStderrWrite.call(process.stderr, str, ...args);
|
||||
};
|
||||
}
|
||||
|
||||
untap() {
|
||||
process.stdout.write = this.origStdoutWrite;
|
||||
process.stderr.write = this.origStderrWrite;
|
||||
if (this.printDots) {
|
||||
console.error('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Diff strings by line, using the 'diff' npm package:
|
||||
* https://www.npmjs.com/package/diff
|
||||
*/
|
||||
export function diffLines(str1: string, str2: string): string {
|
||||
const { diffTrimmedLines } = require('diff');
|
||||
const diffObjs = diffTrimmedLines(str1, str2);
|
||||
const prefix = (chunk: string, char: string) =>
|
||||
chunk
|
||||
.split('\n')
|
||||
.map((line: string) => `${char} ${line}`)
|
||||
.join('\n');
|
||||
const diffStr = diffObjs
|
||||
.map((part: any) => {
|
||||
return part.added
|
||||
? prefix(part.value, '+')
|
||||
: part.removed
|
||||
? prefix(part.value, '-')
|
||||
: prefix(part.value, ' ');
|
||||
})
|
||||
.join('\n');
|
||||
return diffStr;
|
||||
}
|
||||
|
||||
export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
* executable in the PATH environment variable. Does not cache the results,
|
||||
* so hash -r is not needed when the PATH changes."
|
||||
*
|
||||
* @param program Basename of a program, for example 'ssh'
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`'${program}' program not found. Is it installed?`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments. Throw an error
|
||||
* if the process exit code is not zero.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[] = [],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
try {
|
||||
exitCode = await new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
spawn(program, args, { stdio: 'inherit' })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || exitCode) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code ${exitCode}:`,
|
||||
`"${program}" [${args}]`,
|
||||
];
|
||||
if (error) {
|
||||
msg.push(`${error}`);
|
||||
}
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
#!/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 \
|
||||
signup ssh sync util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash logs push scan ssh stop"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
22
bin/balena
22
bin/balena
@ -1,11 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// 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'
|
||||
})
|
||||
require('../build/app');
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
async function run() {
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
await require('../build/fast-boot').start();
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Run the CLI
|
||||
await require('../build/app').run();
|
||||
}
|
||||
|
||||
run();
|
||||
|
@ -1,19 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PERROSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// THIS IS FOR DEV PURPOSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// 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'
|
||||
})
|
||||
process.env['TS_NODE_PROJECT'] = require('path').dirname(__dirname);
|
||||
require('coffeescript/register');
|
||||
require('ts-node/register');
|
||||
require('../lib/app');
|
||||
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||
// behavior when changes are made to dependencies during development. This is
|
||||
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||
// `package.json` to modify oclif paths, and this results in cache
|
||||
// invalidation and a performance hit rather than speedup.
|
||||
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||
// when the `node_modules` folder is manually modified (affecting transitive
|
||||
// dependencies) during development (e.g. bug investigations). A workaround
|
||||
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||
// `CONTRIBUTING.md`.
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
// Allow balena-dev to work with oclif by temporarily
|
||||
// pointing oclif config options to lib/ instead of build/
|
||||
modifyOclifPaths();
|
||||
// Undo changes on exit
|
||||
process.on('exit', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
// Undo changes in case of ctrl-c
|
||||
process.on('SIGINT', function () {
|
||||
modifyOclifPaths(true);
|
||||
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||
// but without it commands can not be exited.
|
||||
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||
// Ideally a better solution is needed.
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// 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();
|
||||
|
||||
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||
function modifyOclifPaths(revert) {
|
||||
const fs = require('fs');
|
||||
const packageJsonPath = path.join(rootDir, 'package.json');
|
||||
|
||||
const packageJson = fs.readFileSync(packageJsonPath, 'utf8');
|
||||
const packageObj = JSON.parse(packageJson);
|
||||
|
||||
if (!packageObj.oclif) {
|
||||
return;
|
||||
}
|
||||
|
||||
let oclifSectionText = JSON.stringify(packageObj.oclif);
|
||||
if (!revert) {
|
||||
oclifSectionText = oclifSectionText.replace(/\/build\//g, '/lib/');
|
||||
} else {
|
||||
oclifSectionText = oclifSectionText.replace(/\/lib\//g, '/build/');
|
||||
}
|
||||
|
||||
packageObj.oclif = JSON.parse(oclifSectionText);
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
`${JSON.stringify(packageObj, null, 2)}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
127
coffeelint.json
127
coffeelint.json
@ -1,127 +0,0 @@
|
||||
{
|
||||
"coffeescript_error": {
|
||||
"level": "error"
|
||||
},
|
||||
"arrow_spacing": {
|
||||
"name": "arrow_spacing",
|
||||
"level": "error"
|
||||
},
|
||||
"no_tabs": {
|
||||
"name": "no_tabs",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_trailing_whitespace": {
|
||||
"name": "no_trailing_whitespace",
|
||||
"level": "error",
|
||||
"allowed_in_comments": false,
|
||||
"allowed_in_empty_lines": false
|
||||
},
|
||||
"max_line_length": {
|
||||
"name": "max_line_length",
|
||||
"value": 120,
|
||||
"level": "error",
|
||||
"limitComments": true
|
||||
},
|
||||
"line_endings": {
|
||||
"name": "line_endings",
|
||||
"level": "ignore",
|
||||
"value": "unix"
|
||||
},
|
||||
"no_trailing_semicolons": {
|
||||
"name": "no_trailing_semicolons",
|
||||
"level": "error"
|
||||
},
|
||||
"indentation": {
|
||||
"name": "indentation",
|
||||
"value": 1,
|
||||
"level": "error"
|
||||
},
|
||||
"camel_case_classes": {
|
||||
"name": "camel_case_classes",
|
||||
"level": "error"
|
||||
},
|
||||
"colon_assignment_spacing": {
|
||||
"name": "colon_assignment_spacing",
|
||||
"level": "error",
|
||||
"spacing": {
|
||||
"left": 0,
|
||||
"right": 1
|
||||
}
|
||||
},
|
||||
"no_implicit_braces": {
|
||||
"name": "no_implicit_braces",
|
||||
"level": "ignore",
|
||||
"strict": false
|
||||
},
|
||||
"no_plusplus": {
|
||||
"name": "no_plusplus",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_throwing_strings": {
|
||||
"name": "no_throwing_strings",
|
||||
"level": "error"
|
||||
},
|
||||
"no_backticks": {
|
||||
"name": "no_backticks",
|
||||
"level": "error"
|
||||
},
|
||||
"no_implicit_parens": {
|
||||
"name": "no_implicit_parens",
|
||||
"strict": false,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_empty_param_list": {
|
||||
"name": "no_empty_param_list",
|
||||
"level": "error"
|
||||
},
|
||||
"no_stand_alone_at": {
|
||||
"name": "no_stand_alone_at",
|
||||
"level": "ignore"
|
||||
},
|
||||
"space_operators": {
|
||||
"name": "space_operators",
|
||||
"level": "error"
|
||||
},
|
||||
"duplicate_key": {
|
||||
"name": "duplicate_key",
|
||||
"level": "error"
|
||||
},
|
||||
"empty_constructor_needs_parens": {
|
||||
"name": "empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"cyclomatic_complexity": {
|
||||
"name": "cyclomatic_complexity",
|
||||
"value": 10,
|
||||
"level": "ignore"
|
||||
},
|
||||
"newlines_after_classes": {
|
||||
"name": "newlines_after_classes",
|
||||
"value": 3,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_fat_arrows": {
|
||||
"name": "no_unnecessary_fat_arrows",
|
||||
"level": "error"
|
||||
},
|
||||
"missing_fat_arrows": {
|
||||
"name": "missing_fat_arrows",
|
||||
"level": "ignore"
|
||||
},
|
||||
"non_empty_constructor_needs_parens": {
|
||||
"name": "non_empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_double_quotes": {
|
||||
"name": "no_unnecessary_double_quotes",
|
||||
"level": "error"
|
||||
},
|
||||
"no_debugger": {
|
||||
"name": "no_debugger",
|
||||
"level": "warn"
|
||||
},
|
||||
"no_interpolation_in_single_quotes": {
|
||||
"name": "no_interpolation_in_single_quotes",
|
||||
"level": "error"
|
||||
}
|
||||
}
|
83
completion/_balena
Normal file
83
completion/_balena
Normal file
@ -0,0 +1,83 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create purge rename restart rm )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
"api-key")
|
||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
||||
;;
|
||||
"config")
|
||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||
;;
|
||||
"device")
|
||||
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
||||
;;
|
||||
"devices")
|
||||
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
|
||||
;;
|
||||
"env")
|
||||
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
||||
;;
|
||||
"fleet")
|
||||
_describe -t fleet_cmds 'fleet_cmd' fleet_cmds "$@" && ret=0
|
||||
;;
|
||||
"internal")
|
||||
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
||||
;;
|
||||
"key")
|
||||
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
|
||||
;;
|
||||
"local")
|
||||
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
||||
;;
|
||||
"os")
|
||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||
;;
|
||||
"release")
|
||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||
;;
|
||||
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
80
completion/balena-completion.bash
Normal file
80
completion/balena-completion.bash
Normal file
@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="build deploy envs fleets join keys leave login logout logs note orgs preload push releases scan settings ssh support tags tunnel version whoami api-key config device device devices env fleet fleet internal key key local os release release tag util"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create purge rename restart rm"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
api-key)
|
||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||
;;
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
device)
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
devices)
|
||||
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
fleet)
|
||||
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
|
||||
;;
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
key)
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
local)
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
175
completion/generate-completion.js
Normal file
175
completion/generate-completion.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const fs = require('fs');
|
||||
const manifestFile = 'oclif.manifest.json';
|
||||
|
||||
commandsFilePath = path.join(rootDir, manifestFile);
|
||||
if (fs.existsSync(commandsFilePath)) {
|
||||
console.log('Generating shell auto completion files...');
|
||||
} else {
|
||||
console.error(`generate-completion.js: Could not find "${manifestFile}"`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||
|
||||
var mainCommands = [];
|
||||
var additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands)) {
|
||||
const cmd = key.split(':');
|
||||
if (cmd.length > 1) {
|
||||
additionalCommands.push(cmd);
|
||||
if (!mainCommands.includes(cmd[0])) {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
} else {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
}
|
||||
const mainCommandsStr = mainCommands.join(' ');
|
||||
|
||||
// GENERATE BASH COMPLETION FILE
|
||||
bashFilePathIn = path.join(__dirname, '/templates/bash.template');
|
||||
bashFilePathOut = path.join(__dirname, 'balena-completion.bash');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(bashFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR BASH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands="' + mainCommandsStr + '"',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' ' +
|
||||
prevElement +
|
||||
')\n COMPREPLY=( $(compgen -W "$' +
|
||||
prevElement2 +
|
||||
'" -- $cur) )\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(bashFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GENERATE ZSH COMPLETION FILE
|
||||
zshFilePathIn = path.join(__dirname, '/templates/zsh.template');
|
||||
zshFilePathOut = path.join(__dirname, '_balena');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(zshFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR ZSH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands=( ' + mainCommandsStr + ' )',
|
||||
);
|
||||
var subCommands = [];
|
||||
var prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' "' +
|
||||
prevElement +
|
||||
'")\n _describe -t ' +
|
||||
prevElement2 +
|
||||
" '" +
|
||||
prevElement +
|
||||
"_cmd' " +
|
||||
prevElement2 +
|
||||
' "$@" && ret=0\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(zshFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
32
completion/templates/bash.template
Normal file
32
completion/templates/bash.template
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
#TEMPLATE FILE FOR BASH COMPLETION#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
$sub_cmds_prev$
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
35
completion/templates/zsh.template
Normal file
35
completion/templates/zsh.template
Normal file
@ -0,0 +1,35 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#TEMPLATE FILE FOR ZSH COMPLETION#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
$sub_cmds_prev$
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
@ -1,112 +0,0 @@
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
It requires collecting some preliminary information _once_.
|
||||
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
You can run this command as many times as you need, putting the new medium (SD card / USB stick) each time.
|
||||
|
||||
But before you can run it you need to collect the parameters and build the configuration file. Keep reading to figure out how to do it.
|
||||
|
||||
|
||||
## Collect all the required parameters.
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
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
|
||||
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
|
||||
guarantees full compatibility between the steps.
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
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.
|
||||
|
||||
## Build the config file
|
||||
|
||||
Interactive device provisioning process often includes collecting some extra device configuration, like the networking mode and wifi credentials.
|
||||
|
||||
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 _./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 _./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
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
cat CONFIG_FILE
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
Now you're ready to run the command in the beginning of this guide.
|
||||
|
||||
Please note again that all of these steps only need to be done once (unless you need to change something), and once all the parameters are collected the main init command can be run unchanged.
|
||||
|
||||
But there are still some nuances to cover, please read below.
|
||||
|
||||
## Nuances
|
||||
|
||||
### `sudo` password on *nix systems
|
||||
|
||||
In order to write the image to the raw device we need the root permissions, this is unavoidable.
|
||||
|
||||
To improve the security we only run the minimal subcommand with `sudo`.
|
||||
|
||||
This means that with the default setup you're interrupted closer to the end of the device init process to enter your sudo password for this subcommand to work.
|
||||
|
||||
There are several ways to eliminate it and make the process fully non-interactive.
|
||||
|
||||
#### Option 1: make passwordless sudo.
|
||||
|
||||
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 balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
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
|
||||
|
||||
As of June 2017 all the supported devices should not require any other interactive configuration.
|
||||
|
||||
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 balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
2067
doc/cli.markdown
2067
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
3631
docs/balena-cli.md
Normal file
3631
docs/balena-cli.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,40 +0,0 @@
|
||||
path = require('path')
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
inlinesource = require('gulp-inline-source')
|
||||
mocha = require('gulp-mocha')
|
||||
shell = require('gulp-shell')
|
||||
packageJSON = require('./package.json')
|
||||
|
||||
OPTIONS =
|
||||
files:
|
||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||
app: 'lib/**/*.coffee'
|
||||
tests: 'tests/**/*.spec.coffee'
|
||||
pages: 'lib/auth/pages/*.ejs'
|
||||
directories:
|
||||
build: 'build/'
|
||||
|
||||
gulp.task 'pages', ->
|
||||
gulp.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages'))
|
||||
|
||||
gulp.task 'coffee', ->
|
||||
gulp.src(OPTIONS.files.app)
|
||||
.pipe(coffee(bare: true, header: true))
|
||||
.pipe(gulp.dest(OPTIONS.directories.build))
|
||||
|
||||
gulp.task 'test', ->
|
||||
gulp.src(OPTIONS.files.tests, read: false)
|
||||
.pipe(mocha({
|
||||
reporter: 'spec'
|
||||
}))
|
||||
|
||||
gulp.task 'build', gulp.series [
|
||||
'coffee',
|
||||
'pages'
|
||||
]
|
||||
|
||||
gulp.task 'watch', gulp.series [ 'build' ], ->
|
||||
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])
|
@ -1,36 +0,0 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const generate: CommandDefinition<{
|
||||
name: string;
|
||||
}> = {
|
||||
signature: 'api-key generate <name>',
|
||||
description: 'Generate a new API key with the given name',
|
||||
help: stripIndent`
|
||||
This command generates a new API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena api-key generate "Jenkins Key"
|
||||
`,
|
||||
async action(params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
balena.models.apiKey
|
||||
.create(params.name)
|
||||
.then(key => {
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
})
|
||||
.finally(done);
|
||||
},
|
||||
};
|
@ -1,173 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.create =
|
||||
signature: 'app create <name>'
|
||||
description: 'create an application'
|
||||
help: '''
|
||||
Use this command to create a new balena application.
|
||||
|
||||
You can specify the application device type with the `--type` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
|
||||
You can see a list of supported device types with
|
||||
|
||||
$ balena devices supported
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app create MyApp
|
||||
$ balena app create MyApp --type raspberry-pi
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
parameter: 'type'
|
||||
description: 'application device type (Check available types with `balena devices supported`)'
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
# Validate the the application name is available
|
||||
# before asking the device type.
|
||||
# https://github.com/balena-io/balena-cli/issues/30
|
||||
balena.models.application.has(params.name).then (hasApplication) ->
|
||||
if hasApplication
|
||||
patterns.exitWithExpectedError('You already have an application with that name!')
|
||||
|
||||
.then ->
|
||||
return options.type or patterns.selectDeviceType()
|
||||
.then (deviceType) ->
|
||||
return balena.models.application.create({
|
||||
name: params.name
|
||||
deviceType
|
||||
})
|
||||
.then (application) ->
|
||||
console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})")
|
||||
.nodeify(done)
|
||||
|
||||
exports.list =
|
||||
signature: 'apps'
|
||||
description: 'list all applications'
|
||||
help: '''
|
||||
Use this command to list all your applications.
|
||||
|
||||
Notice this command only shows the most important bits of information for each app.
|
||||
If you want detailed information, use balena app <name> instead.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena apps
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.application.getAll
|
||||
$select: [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
]
|
||||
$expand: owns__device: $select: 'is_online'
|
||||
.then (applications) ->
|
||||
applications.forEach (application) ->
|
||||
application.device_count = _.size(application.owns__device)
|
||||
application.online_devices = _.filter(application.owns__device, (d) -> d.is_online == true).length
|
||||
|
||||
console.log visuals.table.horizontal applications, [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
'online_devices'
|
||||
'device_count'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'app <name>'
|
||||
description: 'list a single application'
|
||||
help: '''
|
||||
Use this command to show detailed information for a single application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.application.get(params.name).then (application) ->
|
||||
console.log visuals.table.vertical application, [
|
||||
"$#{application.app_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'git_repository'
|
||||
'commit'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.restart =
|
||||
signature: 'app restart <name>'
|
||||
description: 'restart an application'
|
||||
help: '''
|
||||
Use this command to restart all devices that belongs to a certain application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app restart MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.application.restart(params.name).nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'app rm <name>'
|
||||
description: 'remove an application'
|
||||
help: '''
|
||||
Use this command to remove a balena application.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena app rm MyApp
|
||||
$ balena app rm MyApp --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then ->
|
||||
balena.models.application.remove(params.name)
|
||||
.nodeify(done)
|
@ -1,210 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.login =
|
||||
signature: 'login'
|
||||
description: 'login to balena'
|
||||
help: '''
|
||||
Use this command to login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
- Web authorization: open your web browser and prompt you to authorize the CLI
|
||||
from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena login
|
||||
$ balena login --web
|
||||
$ balena login --token "..."
|
||||
$ balena login --credentials
|
||||
$ balena login --credentials --email johndoe@gmail.com --password secret
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'session token or API key'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
{
|
||||
signature: 'web'
|
||||
description: 'web-based login'
|
||||
boolean: true
|
||||
alias: 'w'
|
||||
}
|
||||
{
|
||||
signature: 'credentials'
|
||||
description: 'credential-based login'
|
||||
boolean: true
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
]
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
messages = require('../utils/messages')
|
||||
|
||||
login = (options) ->
|
||||
if options.token?
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Session token or API key from the preferences page'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(balena.auth.loginWithToken)
|
||||
.tap ->
|
||||
balena.auth.whoami()
|
||||
.then (username) ->
|
||||
if !username
|
||||
patterns.exitWithExpectedError('Token authentication failed')
|
||||
else if options.credentials
|
||||
return patterns.authenticate(options)
|
||||
else if options.web
|
||||
console.info('Connecting to the web dashboard')
|
||||
return auth.login()
|
||||
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
return runCommand('signup')
|
||||
|
||||
options[loginType] = true
|
||||
return login(options)
|
||||
|
||||
balena.settings.get('balenaUrl').then (balenaUrl) ->
|
||||
console.log(messages.balenaAsciiArt)
|
||||
console.log("\nLogging in to #{balenaUrl}")
|
||||
return login(options)
|
||||
.then(balena.auth.whoami)
|
||||
.tap (username) ->
|
||||
console.info("Successfully logged in as: #{username}")
|
||||
console.info """
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ balena help
|
||||
|
||||
#{messages.reachingOut}
|
||||
"""
|
||||
.nodeify(done)
|
||||
|
||||
exports.logout =
|
||||
signature: 'logout'
|
||||
description: 'logout from balena'
|
||||
help: '''
|
||||
Use this command to logout from your balena account.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena logout
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.auth.logout().nodeify(done)
|
||||
|
||||
exports.signup =
|
||||
signature: 'signup'
|
||||
description: 'signup to balena'
|
||||
help: '''
|
||||
Use this command to signup for a balena account.
|
||||
|
||||
If signup is successful, you'll be logged in to your new user automatically.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena signup
|
||||
Email: johndoe@acme.com
|
||||
Password: ***********
|
||||
|
||||
$ balena whoami
|
||||
johndoe
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
validation = require('../utils/validation')
|
||||
|
||||
balena.settings.get('balenaUrl').then (balenaUrl) ->
|
||||
console.log("\nRegistering to #{balenaUrl}")
|
||||
|
||||
form.run [
|
||||
message: 'Email:'
|
||||
name: 'email'
|
||||
type: 'input'
|
||||
validate: validation.validateEmail
|
||||
,
|
||||
message: 'Password:'
|
||||
name: 'password'
|
||||
type: 'password',
|
||||
validate: validation.validatePassword
|
||||
]
|
||||
|
||||
.then(balena.auth.register)
|
||||
.then(balena.auth.loginWithToken)
|
||||
.nodeify(done)
|
||||
|
||||
exports.whoami =
|
||||
signature: 'whoami'
|
||||
description: 'get current username and email address'
|
||||
help: '''
|
||||
Use this command to find out the current logged in username and email address.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena whoami
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
username: balena.auth.whoami()
|
||||
email: balena.auth.getEmail()
|
||||
url: balena.settings.get('balenaUrl')
|
||||
.then (results) ->
|
||||
console.log visuals.table.vertical results, [
|
||||
'$account information$'
|
||||
'username'
|
||||
'email'
|
||||
'url'
|
||||
]
|
||||
.nodeify(done)
|
@ -1,154 +0,0 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the app this build is for (optional)
|
||||
arch: the architecture to build for
|
||||
deviceType: the device type to build for
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
buildProject = (docker, logger, composeOpts, opts) ->
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
undefined # image: name of pre-built image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.then (project) ->
|
||||
appType = opts.app?.application_type?[0]
|
||||
if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.'
|
||||
)
|
||||
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
project.composition
|
||||
opts.arch
|
||||
opts.deviceType
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then ->
|
||||
logger.logSuccess('Build succeeded!')
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Build failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'build [source]'
|
||||
description: 'Build a single image or a multicontainer project locally'
|
||||
primary: true
|
||||
help: """
|
||||
Use this command to build an image or a complete multicontainer project with
|
||||
the provided docker daemon in your development machine or balena device.
|
||||
(See also the `balena push` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture pair to use
|
||||
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file. If it is found,
|
||||
this command will build each service defined in the compose file. If a compose
|
||||
file isn't found, the command will look for a Dockerfile[.template] file (or
|
||||
alternative Dockerfile specified with the `-f` option), and if yet that isn't
|
||||
found, it will try to generate one.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena build
|
||||
$ balena build ./source/
|
||||
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||||
$ balena build --application MyApp ./source/
|
||||
$ balena build --docker '/var/run/docker.sock'
|
||||
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
"""
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
description: 'The architecture to build for'
|
||||
alias: 'A'
|
||||
},
|
||||
{
|
||||
signature: 'deviceType'
|
||||
parameter: 'deviceType'
|
||||
description: 'The type of device this build is for'
|
||||
alias: 'd'
|
||||
},
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target balena application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = new Logger()
|
||||
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
Promise.try ->
|
||||
# `build` accepts `[source]` as a parameter, but compose expects it
|
||||
# as an option. swap them here
|
||||
options.source ?= params.source
|
||||
delete params.source
|
||||
|
||||
validateComposeOptions(options)
|
||||
|
||||
{ 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,147 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _ = require('lodash');
|
||||
|
||||
export const yes = {
|
||||
signature: 'yes',
|
||||
description: 'confirm non interactively',
|
||||
boolean: true,
|
||||
alias: 'y',
|
||||
};
|
||||
|
||||
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 const osVersion = _.defaults(
|
||||
{
|
||||
required: 'You have to specify an exact os version',
|
||||
},
|
||||
exports.optionalOsVersion,
|
||||
);
|
||||
|
||||
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,357 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2018 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.read =
|
||||
signature: 'config read'
|
||||
description: 'read a device configuration'
|
||||
help: '''
|
||||
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config read --type raspberry-pi
|
||||
$ balena config read --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
return config.read(drive, options.type)
|
||||
.tap (configJSON) ->
|
||||
console.info(prettyjson.render(configJSON))
|
||||
.nodeify(done)
|
||||
|
||||
exports.write =
|
||||
signature: 'config write <key> <value>'
|
||||
description: 'write a device configuration'
|
||||
help: '''
|
||||
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config write --type raspberry-pi username johndoe
|
||||
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ balena config write --type raspberry-pi files.network/settings "..."
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).then (configJSON) ->
|
||||
console.info("Setting #{params.key} to #{params.value}")
|
||||
_.set(configJSON, params.key, params.value)
|
||||
return configJSON
|
||||
.tap ->
|
||||
return umountAsync(drive)
|
||||
.then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.inject =
|
||||
signature: 'config inject <file>'
|
||||
description: 'inject a device configuration file'
|
||||
help: '''
|
||||
Use this command to inject a config.json file to the mounted filesystem
|
||||
(e.g. SD card or mounted balenaOS image) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config inject my/config.json --type raspberry-pi
|
||||
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
readFileAsync(params.file, 'utf8').then(JSON.parse).then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.reconfigure =
|
||||
signature: 'config reconfigure'
|
||||
description: 'reconfigure a provisioned device'
|
||||
help: '''
|
||||
Use this command to reconfigure a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config reconfigure --type raspberry-pi
|
||||
$ balena config reconfigure --type raspberry-pi --advanced
|
||||
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
{
|
||||
signature: 'advanced'
|
||||
description: 'show advanced commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).get('uuid')
|
||||
.tap ->
|
||||
umountAsync(drive)
|
||||
.then (uuid) ->
|
||||
configureCommand = "os configure #{drive} --device #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return runCommand(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.generate =
|
||||
signature: 'config generate'
|
||||
description: 'generate a config.json file'
|
||||
help: '''
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7
|
||||
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
|
||||
$ balena config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
'''
|
||||
options: [
|
||||
commandOptions.osVersion
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
{
|
||||
signature: 'generate-device-api-key'
|
||||
description: 'generate a fresh device key for the device'
|
||||
boolean: true
|
||||
}
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output'
|
||||
parameter: 'output'
|
||||
alias: 'o'
|
||||
}
|
||||
# Options for non-interactive configuration
|
||||
{
|
||||
signature: 'network'
|
||||
description: 'the network type to use: ethernet or wifi'
|
||||
parameter: 'network'
|
||||
}
|
||||
{
|
||||
signature: 'wifiSsid'
|
||||
description: 'the wifi ssid to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiSsid'
|
||||
}
|
||||
{
|
||||
signature: 'wifiKey'
|
||||
description: 'the wifi key to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiKey'
|
||||
}
|
||||
{
|
||||
signature: 'appUpdatePollInterval'
|
||||
description: 'how frequently (in minutes) to poll for application updates'
|
||||
parameter: 'appUpdatePollInterval'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
helpers = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
exitWithExpectedError '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
if !options.application and options.deviceType
|
||||
exitWithExpectedError '''
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
Promise.try ->
|
||||
if options.device?
|
||||
return balena.models.device.get(options.device)
|
||||
return balena.models.application.get(options.application)
|
||||
.then (resource) ->
|
||||
deviceType = options.deviceType || resource.device_type
|
||||
manifestPromise = balena.models.device.getManifestBySlug(deviceType)
|
||||
|
||||
if options.application && options.deviceType
|
||||
app = resource
|
||||
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
|
||||
manifestPromise = manifestPromise.tap (paramDeviceType) ->
|
||||
appManifestPromise.then (appDeviceType) ->
|
||||
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
"Device type #{options.deviceType} is incompatible with application #{options.application}"
|
||||
)
|
||||
|
||||
manifestPromise.get('options')
|
||||
.then (formOptions) ->
|
||||
# Pass params as an override: if there is any param with exactly the same name as a
|
||||
# required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, override: options)
|
||||
.then (answers) ->
|
||||
answers.version = options.version
|
||||
|
||||
if resource.uuid?
|
||||
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
|
||||
else
|
||||
answers.deviceType = deviceType
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
if options.output?
|
||||
return writeFileAsync(options.output, JSON.stringify(config))
|
||||
|
||||
console.log(prettyjson.render(config))
|
||||
.nodeify(done)
|
@ -1,236 +0,0 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
dockerfilePath: name of an alternative Dockerfile; optional
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
deployProject = (docker, logger, composeOpts, opts) ->
|
||||
_ = require('lodash')
|
||||
doodles = require('resin-doodles')
|
||||
sdk = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
opts.image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.then (project) ->
|
||||
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
|
||||
throw new Error('Target application does not support multiple containers. Aborting!')
|
||||
|
||||
# find which services use images that already exist locally
|
||||
Promise.map project.descriptors, (d) ->
|
||||
# unconditionally build (or pull) if explicitly requested
|
||||
return d if opts.shouldPerformBuild
|
||||
docker.getImage(d.image.tag ? d.image).inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn()
|
||||
.filter (d) -> !!d
|
||||
.then (servicesToSkip) ->
|
||||
# multibuild takes in a composition and always attempts to
|
||||
# build or pull all services. we workaround that here by
|
||||
# passing a modified composition.
|
||||
compositionToBuild = _.cloneDeep(project.composition)
|
||||
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
|
||||
if _.size(compositionToBuild.services) is 0
|
||||
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
|
||||
return {}
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
compositionToBuild
|
||||
opts.app.arch
|
||||
opts.app.device_type
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then (builtImages) ->
|
||||
_.keyBy(builtImages, 'serviceName')
|
||||
.then (builtImages) ->
|
||||
project.descriptors.map (d) ->
|
||||
builtImages[d.serviceName] ? {
|
||||
serviceName: d.serviceName,
|
||||
name: d.image.tag ? d.image
|
||||
logs: 'Build skipped; image for service already exists.'
|
||||
props: {}
|
||||
}
|
||||
.then (images) ->
|
||||
if opts.app.application_type?[0]?.is_legacy
|
||||
chalk = require('chalk')
|
||||
legacyDeploy = require('../utils/deploy-legacy')
|
||||
|
||||
msg = chalk.yellow('Target application requires legacy deploy method.')
|
||||
logger.logWarn(msg)
|
||||
|
||||
return Promise.join(
|
||||
docker
|
||||
logger
|
||||
sdk.auth.getToken()
|
||||
sdk.auth.whoami()
|
||||
sdk.settings.get('balenaUrl')
|
||||
{
|
||||
# opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName
|
||||
imageName: images[0].name
|
||||
buildLogs: images[0].logs
|
||||
shouldUploadLogs: opts.shouldUploadLogs
|
||||
}
|
||||
legacyDeploy
|
||||
)
|
||||
.then (releaseId) ->
|
||||
sdk.models.release.get(releaseId, $select: [ 'commit' ])
|
||||
Promise.join(
|
||||
sdk.auth.getUserId()
|
||||
sdk.auth.getToken()
|
||||
sdk.settings.get('apiUrl')
|
||||
(userId, auth, apiEndpoint) ->
|
||||
compose.deployProject(
|
||||
docker
|
||||
logger
|
||||
project.composition
|
||||
images
|
||||
opts.app.id
|
||||
userId
|
||||
"Bearer #{auth}"
|
||||
apiEndpoint
|
||||
!opts.shouldUploadLogs
|
||||
)
|
||||
)
|
||||
.then (release) ->
|
||||
logger.logSuccess('Deploy succeeded!')
|
||||
logger.logSuccess("Release: #{release.commit}")
|
||||
console.log()
|
||||
console.log(doodles.getDoodle()) # Show charlie
|
||||
console.log()
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Deploy failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'deploy <appName> [image]'
|
||||
description: 'Deploy a single image or a multicontainer project to a balena application'
|
||||
help: """
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the `balena push` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml file. If one is
|
||||
found, this command will deploy each service defined in the compose file,
|
||||
building it first if an image for it doesn't exist. If a compose file isn't
|
||||
found, the command will look for a Dockerfile[.template] file (or alternative
|
||||
Dockerfile specified with the `-f` option), and if yet that isn't found, it
|
||||
will try to generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`balena deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
When --build is used, all options supported by `balena build` are also supported
|
||||
by this command.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage
|
||||
"""
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'Specify an alternate source directory; default is the working directory'
|
||||
alias: 's'
|
||||
},
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Force a rebuild before deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'nologupload'
|
||||
description: "Don't upload build logs to the dashboard with image (if building)"
|
||||
boolean: true
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = new Logger()
|
||||
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
appName = undefined
|
||||
Promise.try ->
|
||||
# when Capitano converts a positional parameter (but not an option)
|
||||
# to a number, the original value is preserved with the _raw suffix
|
||||
{ appName, appName_raw, image } = params
|
||||
|
||||
# look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application
|
||||
delete options.application
|
||||
|
||||
validateComposeOptions(options)
|
||||
|
||||
if not appName?
|
||||
throw new Error('Please specify the name of the application to deploy')
|
||||
|
||||
if image? and options.build
|
||||
throw new Error('Build option is not applicable when specifying an image')
|
||||
|
||||
Promise.join(
|
||||
helpers.getApplication(appName)
|
||||
helpers.getArchAndDeviceType(appName)
|
||||
(app, { arch, device_type }) ->
|
||||
app.arch = arch
|
||||
app.device_type = device_type
|
||||
return app
|
||||
)
|
||||
.then (app) ->
|
||||
[ app, image, !!options.build, !options.nologupload ]
|
||||
|
||||
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app
|
||||
appName # may be prefixed by 'owner/', unlike app.app_name
|
||||
image
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
@ -1,457 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
expandForAppName = {
|
||||
$expand: belongs_to__application: $select: 'app_name'
|
||||
}
|
||||
|
||||
exports.list =
|
||||
signature: 'devices'
|
||||
description: 'list all devices'
|
||||
help: '''
|
||||
Use this command to list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the `--application` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
'''
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return balena.models.device.getAllByApplication(options.application, expandForAppName)
|
||||
return balena.models.device.getAll(expandForAppName)
|
||||
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.uuid = device.uuid.slice(0, 7)
|
||||
return device
|
||||
|
||||
console.log visuals.table.horizontal devices, [
|
||||
'id'
|
||||
'uuid'
|
||||
'device_name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
'is_online'
|
||||
'supervisor_version'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'device <uuid>'
|
||||
description: 'list a single device'
|
||||
help: '''
|
||||
Use this command to show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device 7cf02a6
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.device.get(params.uuid, expandForAppName)
|
||||
.then (device) ->
|
||||
balena.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.commit = device.is_on__commit
|
||||
|
||||
console.log visuals.table.vertical device, [
|
||||
"$#{device.device_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
'is_online'
|
||||
'ip_address'
|
||||
'application_name'
|
||||
'last_seen'
|
||||
'uuid'
|
||||
'commit'
|
||||
'supervisor_version'
|
||||
'is_web_accessible'
|
||||
'note'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.supported =
|
||||
signature: 'devices supported'
|
||||
description: 'list all supported devices'
|
||||
help: '''
|
||||
Use this command to get the list of all supported devices
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena devices supported
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.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.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'uuid'
|
||||
description: 'custom uuid'
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.join(
|
||||
balena.models.application.get(params.application)
|
||||
options.uuid ? balena.models.device.generateUniqueKey()
|
||||
(application, uuid) ->
|
||||
console.info("Registering to #{application.app_name}: #{uuid}")
|
||||
return balena.models.device.register(application.id, uuid)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'device rm <uuid>'
|
||||
description: 'remove a device'
|
||||
help: '''
|
||||
Use this command to remove a device from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
|
||||
balena.models.device.remove(params.uuid)
|
||||
.nodeify(done)
|
||||
|
||||
exports.identify =
|
||||
signature: 'device identify <uuid>'
|
||||
description: 'identify a device with a UUID'
|
||||
help: '''
|
||||
Use this command to identify a device.
|
||||
|
||||
In the Raspberry Pi, the ACT led is blinked several times.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device identify 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
signature: 'device reboot <uuid>'
|
||||
description: 'restart a device'
|
||||
help: '''
|
||||
Use this command to remotely reboot a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device reboot 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.shutdown =
|
||||
signature: 'device shutdown <uuid>'
|
||||
description: 'shutdown a device'
|
||||
help: '''
|
||||
Use this command to remotely shutdown a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device shutdown 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.shutdown(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.enableDeviceUrl =
|
||||
signature: 'device public-url enable <uuid>'
|
||||
description: 'enable public URL for a device'
|
||||
help: '''
|
||||
Use this command to enable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url enable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
signature: 'device public-url disable <uuid>'
|
||||
description: 'disable public URL for a device'
|
||||
help: '''
|
||||
Use this command to disable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url disable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
signature: 'device public-url <uuid>'
|
||||
description: 'gets the public URL of a device'
|
||||
help: '''
|
||||
Use this command to get the public URL of a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
|
||||
exports.hasDeviceUrl =
|
||||
signature: 'device public-url status <uuid>'
|
||||
description: 'Returns true if public URL is enabled for a device'
|
||||
help: '''
|
||||
Use this command to determine if public URL is enabled for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device public-url status 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
console.log(hasDeviceUrl)
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'device rename <uuid> [newName]'
|
||||
description: 'rename a balena device'
|
||||
help: '''
|
||||
Use this command to rename a device.
|
||||
|
||||
If you omit the name, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
return params.newName if not _.isEmpty(params.newName)
|
||||
|
||||
form.ask
|
||||
message: 'How do you want to name this device?'
|
||||
type: 'input'
|
||||
|
||||
.then(_.partial(balena.models.device.rename, params.uuid))
|
||||
.nodeify(done)
|
||||
|
||||
exports.move =
|
||||
signature: 'device move <uuid>'
|
||||
description: 'move a device to another application'
|
||||
help: '''
|
||||
Use this command to move a device to another application you own.
|
||||
|
||||
If you omit the application, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6 --application MyNewApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
balena.models.device.get(params.uuid, expandForAppName).then (device) ->
|
||||
return options.application or patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
application.device_type is device.device_type
|
||||
device.belongs_to__application[0].app_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return balena.models.device.move(params.uuid, application)
|
||||
.then (application) ->
|
||||
console.info("#{params.uuid} was moved to #{application}")
|
||||
.nodeify(done)
|
||||
|
||||
exports.init =
|
||||
signature: 'device init'
|
||||
description: 'initialise a device with balenaOS'
|
||||
help: '''
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
Notice this command may ask for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.yes
|
||||
commandOptions.advancedConfig
|
||||
_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
|
||||
commandOptions.drive
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
rimraf = Promise.promisify(require('rimraf'))
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
|
||||
Promise.try ->
|
||||
return options.application if options.application?
|
||||
return patterns.selectApplication()
|
||||
.then(balena.models.application.get)
|
||||
.then (application) ->
|
||||
|
||||
download = ->
|
||||
tmpNameAsync().then (tempPath) ->
|
||||
osVersion = options['os-version'] or 'default'
|
||||
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
runCommand("device register #{application.app_name}")
|
||||
.then(balena.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
|
||||
if options.config
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
runCommand(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
runCommand(osInitCommand)
|
||||
# Make sure the device resource is removed if there is an
|
||||
# error when configuring or initializing a device image
|
||||
.catch (error) ->
|
||||
balena.models.device.remove(device.uuid).finally ->
|
||||
throw error
|
||||
.then (device) ->
|
||||
console.log('Done')
|
||||
return device.uuid
|
||||
|
||||
.nodeify(done)
|
@ -1,306 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApplicationVariable, DeviceVariable } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
const getReservedPrefixes = async (): 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;
|
||||
};
|
||||
|
||||
export const list: CommandDefinition<
|
||||
{},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
config: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'envs',
|
||||
description: 'list all environment variables',
|
||||
help: stripIndent`
|
||||
Use this command to list the environment variables of an application
|
||||
or device.
|
||||
|
||||
The --config option is used to list "config" variables that configure
|
||||
balena features.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples list variables that apply to all services in an app or device.
|
||||
|
||||
Example:
|
||||
|
||||
$ balena envs --application MyApp
|
||||
$ balena envs --application MyApp --config
|
||||
$ balena envs --device 7cf02a6
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
|
||||
{
|
||||
signature: 'config',
|
||||
description: 'show config variables',
|
||||
boolean: true,
|
||||
alias: ['c', 'v', 'verbose'],
|
||||
},
|
||||
],
|
||||
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(function(): Promise<
|
||||
DeviceVariable[] | ApplicationVariable[]
|
||||
> {
|
||||
if (options.application) {
|
||||
return balena.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(options.application);
|
||||
} else if (options.device) {
|
||||
return balena.models.device[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByDevice(options.device);
|
||||
} else {
|
||||
return exitWithExpectedError(
|
||||
'You must specify an application or device',
|
||||
);
|
||||
}
|
||||
})
|
||||
.tap(function(environmentVariables) {
|
||||
if (_.isEmpty(environmentVariables)) {
|
||||
exitWithExpectedError('No environment variables found');
|
||||
}
|
||||
|
||||
console.log(
|
||||
visuals.table.horizontal(environmentVariables, [
|
||||
'id',
|
||||
'name',
|
||||
'value',
|
||||
]),
|
||||
);
|
||||
})
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const remove: CommandDefinition<
|
||||
{
|
||||
id: number;
|
||||
},
|
||||
{
|
||||
yes: boolean;
|
||||
device: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'env rm <id>',
|
||||
description: 'remove an environment variable',
|
||||
help: stripIndent`
|
||||
Use this command to remove an environment variable from an application
|
||||
or device.
|
||||
|
||||
Notice 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.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples remove variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env rm 215
|
||||
$ balena env rm 215 --yes
|
||||
$ balena env rm 215 --device
|
||||
`,
|
||||
options: [commandOptions.yes, commandOptions.booleanDevice],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const patterns = await import('../utils/patterns');
|
||||
|
||||
return patterns
|
||||
.confirm(
|
||||
options.yes,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
)
|
||||
.then(function() {
|
||||
if (options.device) {
|
||||
return balena.pine.delete({
|
||||
resource: 'device_environment_variable',
|
||||
id: params.id,
|
||||
});
|
||||
} else {
|
||||
return balena.pine.delete({
|
||||
resource: 'application_environment_variable',
|
||||
id: params.id,
|
||||
});
|
||||
}
|
||||
})
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const add: CommandDefinition<
|
||||
{
|
||||
key: string;
|
||||
value?: string;
|
||||
},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
}
|
||||
> = {
|
||||
signature: 'env add <key> [value]',
|
||||
description: 'add an environment or config variable',
|
||||
help: stripIndent`
|
||||
Use this command to add an enviroment or config variable to an application
|
||||
or device.
|
||||
|
||||
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.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples set variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env add EDITOR vim --application MyApp
|
||||
$ balena env add TERM --application MyApp
|
||||
$ balena env add EDITOR vim --device 7cf02a6
|
||||
`,
|
||||
options: [commandOptions.optionalApplication, commandOptions.optionalDevice],
|
||||
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 function() {
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.key];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(`Environment value not found for key: ${params.key}`);
|
||||
} else {
|
||||
console.info(
|
||||
`Warning: using ${params.key}=${
|
||||
params.value
|
||||
} from host environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
_.startsWith(params.key, prefix),
|
||||
);
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application[
|
||||
isConfigVar ? 'configVar' : 'envVar'
|
||||
].set(options.application, params.key, params.value);
|
||||
} else if (options.device) {
|
||||
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
|
||||
options.device,
|
||||
params.key,
|
||||
params.value,
|
||||
);
|
||||
} else {
|
||||
exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const rename: CommandDefinition<
|
||||
{
|
||||
id: number;
|
||||
value: string;
|
||||
},
|
||||
{
|
||||
device: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'env rename <id> <value>',
|
||||
description: 'rename an environment variable',
|
||||
help: stripIndent`
|
||||
Use this command to change the value of an application or device
|
||||
enviroment variable.
|
||||
|
||||
The --device option selects a device instead of an application.
|
||||
|
||||
Service-specific variables are not currently supported. The following
|
||||
examples modify variables that apply to all services in an app or device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena env rename 376 emacs
|
||||
$ balena env rename 376 emacs --device
|
||||
`,
|
||||
permission: 'user',
|
||||
options: [commandOptions.booleanDevice],
|
||||
async action(params, options, done) {
|
||||
const Bluebird = await import('bluebird');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
return Bluebird.try(function() {
|
||||
if (options.device) {
|
||||
return balena.pine.patch({
|
||||
resource: 'device_environment_variable',
|
||||
id: params.id,
|
||||
body: {
|
||||
value: params.value,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return balena.pine.patch({
|
||||
resource: 'application_environment_variable',
|
||||
id: params.id,
|
||||
body: {
|
||||
value: params.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,119 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
messages = require('../utils/messages')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
parse = (object) ->
|
||||
return _.fromPairs _.map(object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
if item.alias?
|
||||
signature = item.toString()
|
||||
else
|
||||
signature = item.signature.toString()
|
||||
|
||||
return [
|
||||
signature
|
||||
item.description
|
||||
]).sort()
|
||||
|
||||
indent = (text) ->
|
||||
text = _.map text.split('\n'), (line) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (data) ->
|
||||
console.log indent columnify data,
|
||||
showHeaders: false
|
||||
minWidth: 35
|
||||
|
||||
general = (params, options, done) ->
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
|
||||
console.log(messages.reachingOut)
|
||||
console.log('\nPrimary commands:\n')
|
||||
|
||||
# We do not want the wildcard command
|
||||
# to be printed in the help screen.
|
||||
commands = _.reject capitano.state.commands, (command) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
|
||||
print(parse(groupedCommands.primary))
|
||||
|
||||
if options.verbose
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
else
|
||||
console.log('\nRun `balena help --verbose` to list additional commands')
|
||||
|
||||
if not _.isEmpty(capitano.state.globalOptions)
|
||||
console.log('\nGlobal Options:\n')
|
||||
print(parse(capitano.state.globalOptions))
|
||||
|
||||
return done()
|
||||
|
||||
command = (params, options, done) ->
|
||||
capitano.state.getMatchCommand params.command, (error, command) ->
|
||||
return done(error) if error?
|
||||
|
||||
if not command? or command.isWildcard()
|
||||
exitWithExpectedError("Command not found: #{params.command}")
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
if command.help?
|
||||
console.log("\n#{command.help}")
|
||||
else if command.description?
|
||||
console.log("\n#{_.capitalize(command.description)}")
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
console.log('\nOptions:\n')
|
||||
print(parse(command.options))
|
||||
|
||||
return done()
|
||||
|
||||
exports.help =
|
||||
signature: 'help [command...]'
|
||||
description: 'show help'
|
||||
help: '''
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena help apps
|
||||
$ balena help os download
|
||||
'''
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
description: 'show additional commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
if params.command?
|
||||
command(params, options, done)
|
||||
else
|
||||
general(params, options, done)
|
@ -1,43 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
apiKey: require('./api-key')
|
||||
app: require('./app')
|
||||
auth: require('./auth')
|
||||
info: require('./info')
|
||||
device: require('./device')
|
||||
env: require('./environment-variables')
|
||||
tags: require('./tags')
|
||||
keys: require('./keys')
|
||||
logs: require('./logs')
|
||||
local: require('./local')
|
||||
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,88 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# These are internal commands we want to be runnable from the outside
|
||||
# One use-case for this is spawning the minimal operation with root priviledges
|
||||
|
||||
exports.osInit =
|
||||
signature: 'internal osinit <image> <type> <config>'
|
||||
description: 'do actual init of the device with the preconfigured os image'
|
||||
help: '''
|
||||
Don't use this command directly! Use `balena os initialize <image>` instead.
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
configPromise = Promise.try -> JSON.parse(params.config)
|
||||
manifestPromise = helpers.getManifest(params.image, params.type)
|
||||
Promise.join configPromise, manifestPromise, (config, manifest) ->
|
||||
init.initialize(params.image, manifest, config)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
exports.scanDevices =
|
||||
signature: 'internal scandevices'
|
||||
description: 'scan for local balena-enabled devices and show a picker to choose one'
|
||||
help: '''
|
||||
Don't use this command directly!
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
return Promise.try ->
|
||||
forms.selectLocalBalenaOsDevice()
|
||||
.then (hostnameOrIp) ->
|
||||
console.error("==> Selected device: #{hostnameOrIp}")
|
||||
.nodeify(done)
|
||||
|
||||
exports.sudo =
|
||||
signature: 'internal sudo <command>'
|
||||
description: 'execute arbitrary commands in a privileged subprocess'
|
||||
help: '''
|
||||
Don't use this command directly!
|
||||
|
||||
<command> must be passed as a single argument. That means, you need to make sure
|
||||
you enclose <command> in quotes (eg. balena internal sudo 'ls -alF') if for
|
||||
whatever reason you invoke the command directly or, typically, pass <command>
|
||||
as a single argument to spawn (eg. `spawn('balena', [ 'internal', 'sudo', 'ls -alF' ])`).
|
||||
|
||||
Furthermore, this command will naively split <command> on whitespace and directly
|
||||
forward the parts as arguments to `sudo`, so be careful.
|
||||
'''
|
||||
hidden: true
|
||||
action: (params, options, done) ->
|
||||
os = require('os')
|
||||
Promise = require('bluebird')
|
||||
|
||||
return Promise.try ->
|
||||
if os.platform() is 'win32'
|
||||
windosu = require('windosu')
|
||||
windosu.exec(params.command, {})
|
||||
else
|
||||
{ spawn } = require('child_process')
|
||||
{ wait } = require('rindle')
|
||||
cmd = params.command.split(' ')
|
||||
ps = spawn('sudo', cmd, stdio: 'inherit', env: process.env)
|
||||
wait(ps)
|
||||
|
||||
.nodeify(done)
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
application?: string;
|
||||
}
|
||||
|
||||
export const join: CommandDefinition<Args, Options> = {
|
||||
signature: 'join [deviceIp]',
|
||||
description:
|
||||
'Promote a local device running balenaOS to join an application on a balena server',
|
||||
help: stripIndent`
|
||||
Use this command to move a local device to an application on another balena server.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
Moving a device between applications on the same server is not supported.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena join
|
||||
$ balena join balena.local
|
||||
$ balena join balena.local --application MyApp
|
||||
$ balena join 192.168.1.25
|
||||
$ balena join 192.168.1.25 --application MyApp
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
alias: 'a',
|
||||
description: 'The name of the application the device should join',
|
||||
},
|
||||
],
|
||||
|
||||
primary: true,
|
||||
|
||||
async action(params, options, done) {
|
||||
const balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.join(logger, sdk, params.deviceIp, options.application);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,123 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.list =
|
||||
signature: 'keys'
|
||||
description: 'list all ssh keys'
|
||||
help: '''
|
||||
Use this command to list all your SSH keys.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena keys
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.key.getAll().then (keys) ->
|
||||
console.log visuals.table.horizontal keys, [
|
||||
'id'
|
||||
'title'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'key <id>'
|
||||
description: 'list a single ssh key'
|
||||
help: '''
|
||||
Use this command to show information about a single SSH key.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key 17
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
balena.models.key.get(params.id).then (key) ->
|
||||
console.log visuals.table.vertical key, [
|
||||
'id'
|
||||
'title'
|
||||
]
|
||||
|
||||
# Since the public key string is long, it might
|
||||
# wrap to lines below, causing the table layout to break.
|
||||
# See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key)
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'key rm <id>'
|
||||
description: 'remove a ssh key'
|
||||
help: '''
|
||||
Use this command to remove a SSH key from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key rm 17
|
||||
$ balena key rm 17 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
|
||||
balena.models.key.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'key add <name> [path]'
|
||||
description: 'add a SSH key to balena'
|
||||
help: '''
|
||||
Use this command to associate a new SSH key with your account.
|
||||
|
||||
If `path` is omitted, the command will attempt
|
||||
to read the SSH key from stdin.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | balena key add Main
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
capitano = require('capitano')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.try ->
|
||||
return readFileAsync(params.path, encoding: 'utf8') if params.path?
|
||||
|
||||
# TODO: should this be promisified for consistency?
|
||||
Promise.fromNode (callback) ->
|
||||
capitano.utils.getStdin (data) ->
|
||||
return callback(null, data)
|
||||
|
||||
.then(_.partial(balena.models.key.create, params.name))
|
||||
.nodeify(done)
|
@ -1,59 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
export const leave: CommandDefinition<Args, {}> = {
|
||||
signature: 'leave [deviceIp]',
|
||||
description: 'Detach a local device from its balena application',
|
||||
help: stripIndent`
|
||||
Use this command to make a local device leave the balena server it is
|
||||
provisioned on. This effectively makes the device "unmanaged".
|
||||
|
||||
The device entry on the server is preserved after running this command,
|
||||
so the device can subsequently re-join the server if needed.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena leave
|
||||
$ balena leave balena.local
|
||||
$ balena leave 192.168.1.25
|
||||
`,
|
||||
options: [],
|
||||
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
|
||||
async action(params, _options, done) {
|
||||
const balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.leave(logger, sdk, params.deviceIp);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
exports.dockerPort = dockerPort = 2375
|
||||
exports.dockerTimeout = dockerTimeout = 2000
|
||||
|
||||
exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container) ->
|
||||
for name in container.Names
|
||||
return false if (name.includes('resin_supervisor') or name.includes('balena_supervisor'))
|
||||
return true
|
||||
|
||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||
form = require('resin-cli-form')
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
|
||||
# List all containers, including those not running
|
||||
docker.listContainersAsync(all: true)
|
||||
.filter (container) ->
|
||||
return true if not filterSupervisor
|
||||
filterOutSupervisorContainer(container)
|
||||
.then (containers) ->
|
||||
if _.isEmpty(containers)
|
||||
exitWithExpectedError("No containers found in #{deviceIp}")
|
||||
|
||||
return form.ask
|
||||
message: 'Select a container'
|
||||
type: 'list'
|
||||
choices: _.map containers, (container) ->
|
||||
containerName = container.Names?[0] or 'Untitled'
|
||||
shortContainerId = ('' + container.Id).substr(0, 11)
|
||||
|
||||
return {
|
||||
name: "#{containerName} (#{shortContainerId})"
|
||||
value: container.Id
|
||||
}
|
||||
|
||||
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
|
||||
|
||||
container = docker.getContainer(name)
|
||||
container.inspectAsync()
|
||||
.then (containerInfo) ->
|
||||
return containerInfo?.State?.Running
|
||||
.then (isRunning) ->
|
||||
container.attachAsync
|
||||
logs: not follow or not isRunning
|
||||
stream: follow and isRunning
|
||||
stdout: true
|
||||
stderr: true
|
||||
.then (containerStream) ->
|
||||
containerStream.pipe(outStream)
|
||||
.catch (err) ->
|
||||
err = '' + err.statusCode
|
||||
if err is '404'
|
||||
return console.log(chalk.red.bold("Container '#{name}' not found."))
|
||||
throw err
|
||||
|
||||
exports.getSubShellCommand = require('../../utils/helpers').getSubShellCommand
|
@ -1,230 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
mapper: [
|
||||
{
|
||||
template:
|
||||
persistentLogging: '{{persistentLogging}}'
|
||||
domain: [
|
||||
[ 'config_json', 'persistentLogging' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
hostname: '{{hostname}}'
|
||||
domain: [
|
||||
[ 'config_json', 'hostname' ]
|
||||
]
|
||||
}
|
||||
{
|
||||
template:
|
||||
wifi:
|
||||
ssid: '{{networkSsid}}'
|
||||
'wifi-security':
|
||||
psk: '{{networkKey}}'
|
||||
domain: [
|
||||
[ 'system_connections', connnectionFileName, 'wifi' ]
|
||||
[ 'system_connections', connnectionFileName, 'wifi-security' ]
|
||||
]
|
||||
}
|
||||
]
|
||||
files:
|
||||
system_connections:
|
||||
fileset: true
|
||||
type: 'ini'
|
||||
location:
|
||||
path: CONNECTIONS_FOLDER.slice(1)
|
||||
# Reconfix still uses the older resin-image-fs, so still needs an
|
||||
# object-based partition definition.
|
||||
partition: BOOT_PARTITION
|
||||
config_json:
|
||||
type: 'json'
|
||||
location:
|
||||
path: 'config.json'
|
||||
partition: BOOT_PARTITION
|
||||
|
||||
inquirerOptions = (data) -> [
|
||||
{
|
||||
message: 'Network SSID'
|
||||
type: 'input'
|
||||
name: 'networkSsid'
|
||||
default: data.networkSsid
|
||||
}
|
||||
{
|
||||
message: 'Network Key'
|
||||
type: 'input'
|
||||
name: 'networkKey'
|
||||
default: data.networkKey
|
||||
}
|
||||
{
|
||||
message: 'Do you want to set advanced settings?'
|
||||
type: 'confirm'
|
||||
name: 'advancedSettings'
|
||||
default: false
|
||||
}
|
||||
{
|
||||
message: 'Device Hostname'
|
||||
type: 'input'
|
||||
name: 'hostname'
|
||||
default: data.hostname,
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
{
|
||||
message: 'Do you want to enable persistent logging?'
|
||||
type: 'confirm'
|
||||
name: 'persistentLogging'
|
||||
default: data.persistentLogging
|
||||
when: (answers) ->
|
||||
answers.advancedSettings
|
||||
}
|
||||
]
|
||||
|
||||
getConfiguration = (data) ->
|
||||
_ = require('lodash')
|
||||
inquirer = require('inquirer')
|
||||
|
||||
# `persistentLogging` can be `undefined`, so we want
|
||||
# to make sure that case defaults to `false`
|
||||
data = _.assign data,
|
||||
persistentLogging: data.persistentLogging or false
|
||||
|
||||
inquirer.prompt(inquirerOptions(data))
|
||||
.then (answers) ->
|
||||
return _.merge(data, answers)
|
||||
|
||||
# Taken from https://goo.gl/kr1kCt
|
||||
CONNECTION_FILE = '''
|
||||
[connection]
|
||||
id=resin-wifi
|
||||
type=wifi
|
||||
|
||||
[wifi]
|
||||
hidden=true
|
||||
mode=infrastructure
|
||||
ssid=My_Wifi_Ssid
|
||||
|
||||
[wifi-security]
|
||||
auth-alg=open
|
||||
key-mgmt=wpa-psk
|
||||
psk=super_secret_wifi_password
|
||||
|
||||
[ipv4]
|
||||
method=auto
|
||||
|
||||
[ipv6]
|
||||
addr-gen-mode=stable-privacy
|
||||
method=auto
|
||||
'''
|
||||
|
||||
###
|
||||
* if the `resin-wifi` file exists (previously configured image or downloaded from the UI) it's used and reconfigured
|
||||
* if the `resin-sample.ignore` exists it's copied to `resin-wifi`
|
||||
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
|
||||
* otherwise, the new file is created
|
||||
###
|
||||
prepareConnectionFile = (target) ->
|
||||
_ = require('lodash')
|
||||
imagefs = require('resin-image-fs')
|
||||
|
||||
imagefs.listDirectory
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: CONNECTIONS_FOLDER
|
||||
.then (files) ->
|
||||
# The required file already exists
|
||||
if _.includes(files, 'resin-wifi')
|
||||
return null
|
||||
|
||||
# Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if _.includes(files, 'resin-sample.ignore')
|
||||
return imagefs.copy
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-sample.ignore"
|
||||
,
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
.thenReturn(null)
|
||||
|
||||
# Legacy mode, to be removed later
|
||||
# We return the file name override from this branch
|
||||
# When it is removed the following cleanup should be done:
|
||||
# * delete all the null returns from this method
|
||||
# * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi`
|
||||
# * drop the final `then` from this method
|
||||
# * adapt the code in the main listener to not receive the config from this method, and use that constant instead
|
||||
if _.includes(files, 'resin-sample')
|
||||
return 'resin-sample'
|
||||
|
||||
# In case there's no file at all (shouldn't happen normally, but the file might have been removed)
|
||||
return imagefs.writeFile
|
||||
image: target
|
||||
partition: BOOT_PARTITION
|
||||
path: "#{CONNECTIONS_FOLDER}/resin-wifi"
|
||||
, CONNECTION_FILE
|
||||
.thenReturn(null)
|
||||
|
||||
.then (connectionFileName) ->
|
||||
return getConfigurationSchema(connectionFileName)
|
||||
|
||||
removeHostname = (schema) ->
|
||||
_ = require('lodash')
|
||||
schema.mapper = _.reject schema.mapper, (mapper) ->
|
||||
_.isEqual(Object.keys(mapper.template), ['hostname'])
|
||||
|
||||
module.exports =
|
||||
signature: 'local configure <target>'
|
||||
description: '(Re)configure a balenaOS drive or image'
|
||||
help: '''
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img
|
||||
'''
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
umount = require('umount')
|
||||
umountAsync = Promise.promisify(umount.umount)
|
||||
isMountedAsync = Promise.promisify(umount.isMounted)
|
||||
reconfix = require('reconfix')
|
||||
denymount = Promise.promisify(require('denymount'))
|
||||
|
||||
prepareConnectionFile(params.target)
|
||||
.tap ->
|
||||
isMountedAsync(params.target).then (isMounted) ->
|
||||
return if not isMounted
|
||||
umountAsync(params.target)
|
||||
.then (configurationSchema) ->
|
||||
denymount params.target, (cb) ->
|
||||
reconfix.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then (answers) ->
|
||||
if not answers.hostname
|
||||
removeHostname(configurationSchema)
|
||||
reconfix.writeConfiguration(configurationSchema, answers, params.target)
|
||||
.asCallback(cb)
|
||||
.then ->
|
||||
console.log('Done!')
|
||||
.asCallback(done)
|
@ -1,120 +0,0 @@
|
||||
/*
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
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,66 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# A function to reliably execute a command
|
||||
# in all supported operating systems, including
|
||||
# different Windows environments like `cmd.exe`
|
||||
# and `Cygwin` should be encapsulated in a
|
||||
# re-usable package.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local logs [deviceIp]'
|
||||
description: 'Get or attach to logs of a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local logs
|
||||
$ balena local logs -f
|
||||
$ balena local logs 192.168.1.10
|
||||
$ balena local logs 192.168.1.10 -f
|
||||
$ balena local logs 192.168.1.10 -f --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'follow'
|
||||
boolean: true
|
||||
description: 'follow log'
|
||||
alias: 'f'
|
||||
,
|
||||
signature: 'app-name'
|
||||
parameter: 'name'
|
||||
description: 'name of container to get logs from'
|
||||
alias: 'a'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('balena-sync')
|
||||
{ selectContainerFromDevice, pipeContainerStream } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
if not options['app-name']?
|
||||
return selectContainerFromDevice(@deviceIp)
|
||||
return options['app-name']
|
||||
.then (appName) =>
|
||||
pipeContainerStream
|
||||
deviceIp: @deviceIp
|
||||
name: appName
|
||||
outStream: process.stdout
|
||||
follow: options['follow']
|
@ -1,95 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# Loads '.balena-sync.yml' configuration from 'source' directory.
|
||||
# Returns the configuration object on success
|
||||
#
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
balenaPush = require('balena-sync').capitano('balena-toolbox')
|
||||
originalAction = balenaPush.action
|
||||
|
||||
# TODO: This is a temporary workaround to reuse the existing `rdt push`
|
||||
# capitano frontend in `balena local push`.
|
||||
|
||||
# coffeelint: disable-next-line ("Line ends with trailing whitespace")
|
||||
deprecationMsg = '''
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Deprecation notice: `balena local push` is deprecated and will be removed in a
|
||||
future release of the CLI. Please use `balena push <ipAddress>` instead.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
'''
|
||||
|
||||
balenaPushHelp = """#{deprecationMsg}
|
||||
Use this command to push your local changes to a container on a LAN-accessible
|
||||
balenaOS device on the fly.
|
||||
|
||||
This command requires an openssh-compatible 'ssh' client and 'rsync' to be
|
||||
available in the executable PATH of the shell environment. For more information
|
||||
(including Windows support) please check the README at:
|
||||
https://github.com/balena-io/balena-cli
|
||||
|
||||
If `Dockerfile` or any file in the 'build-triggers' list is changed,
|
||||
a new container will be built and run on your device.
|
||||
If not, changes will simply be synced with `rsync` into the application container.
|
||||
|
||||
After every 'balena local push' the updated settings will be saved in
|
||||
'<source>/.balena-sync.yml' and will be used in later invocations. You can
|
||||
also change any option by editing '.balena-sync.yml' directly.
|
||||
|
||||
Here is an example '.balena-sync.yml' :
|
||||
|
||||
$ cat $PWD/.balena-sync.yml
|
||||
local_balenaos:
|
||||
app-name: local-app
|
||||
build-triggers:
|
||||
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
environment:
|
||||
- MY_VARIABLE=123
|
||||
|
||||
|
||||
Command line options have precedence over the ones saved in '.balena-sync.yml'.
|
||||
|
||||
If '.gitignore' is found in the source directory then all explicitly listed files will be
|
||||
excluded when using rsync to update the container. You can choose to change this default behavior with the
|
||||
'--skip-gitignore' option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local push
|
||||
$ balena local push --app-name test-server --build-triggers package.json,requirements.txt
|
||||
$ balena local push --force-build
|
||||
$ balena local push --force-build --skip-logs
|
||||
$ balena local push --ignore lib/
|
||||
$ balena local push --verbose false
|
||||
$ balena local push 192.168.2.10 --source . --destination /usr/src/app
|
||||
$ balena local push 192.168.2.10 -s /home/user/balenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
|
||||
"""
|
||||
|
||||
|
||||
|
||||
module.exports = _.assign balenaPush,
|
||||
signature: 'local push [deviceIp]'
|
||||
description: '[deprecated: use "balena push ipAddress"] ' + balenaPush.description
|
||||
help: balenaPushHelp
|
||||
primary: false
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
console.log deprecationMsg
|
||||
originalAction(params, options, done)
|
@ -1,100 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
dockerInfoProperties = [
|
||||
'Containers'
|
||||
'ContainersRunning'
|
||||
'ContainersPaused'
|
||||
'ContainersStopped'
|
||||
'Images'
|
||||
'Driver'
|
||||
'SystemTime'
|
||||
'KernelVersion'
|
||||
'OperatingSystem'
|
||||
'Architecture'
|
||||
]
|
||||
|
||||
dockerVersionProperties = [
|
||||
'Version'
|
||||
'ApiVersion'
|
||||
]
|
||||
|
||||
module.exports =
|
||||
signature: 'local scan'
|
||||
description: 'Scan for balenaOS devices in your local network'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local scan
|
||||
$ balena local scan --timeout 120
|
||||
$ balena local scan --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'Display full info'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'timeout'
|
||||
parameter: 'timeout'
|
||||
description: 'Scan timeout in seconds'
|
||||
alias: 't'
|
||||
]
|
||||
primary: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
prettyjson = require('prettyjson')
|
||||
{ discover } = require('balena-sync')
|
||||
{ SpinnerPromise } = require('resin-cli-visuals')
|
||||
{ dockerPort, dockerTimeout } = require('./common')
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if options.timeout?
|
||||
options.timeout *= 1000
|
||||
|
||||
Promise.try ->
|
||||
new SpinnerPromise
|
||||
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local balenaOS devices..'
|
||||
stopMessage: 'Reporting scan results'
|
||||
.filter ({ address }) ->
|
||||
Promise.try ->
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker.pingAsync()
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
.tap (devices) ->
|
||||
if _.isEmpty(devices)
|
||||
exitWithExpectedError('Could not find any balenaOS devices in the local network')
|
||||
.map ({ host, address }) ->
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
Promise.props
|
||||
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
|
||||
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
|
||||
.then ({ dockerInfo, dockerVersion }) ->
|
||||
|
||||
if not options.verbose
|
||||
dockerInfo = _.pick(dockerInfo, dockerInfoProperties) if _.isObject(dockerInfo)
|
||||
dockerVersion = _.pick(dockerVersion, dockerVersionProperties) if _.isObject(dockerVersion)
|
||||
|
||||
return { host, address, dockerInfo, dockerVersion }
|
||||
.then (devicesInfo) ->
|
||||
console.log(prettyjson.render(devicesInfo, noColor: true))
|
||||
.nodeify(done)
|
@ -1,114 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ hostOSAccess } = require('../command-options')
|
||||
_ = require('lodash')
|
||||
|
||||
localHostOSAccessOption = _.cloneDeep(hostOSAccess)
|
||||
localHostOSAccessOption.description = 'get a shell into the host OS'
|
||||
|
||||
module.exports =
|
||||
signature: 'local ssh [deviceIp]'
|
||||
description: 'Get a shell into a balenaOS device'
|
||||
help: '''
|
||||
Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check the README here: https://github.com/balena-io/balena-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
The '--host' option will get you a shell into the Host OS of the balenaOS device.
|
||||
No option will return a list of containers to enter or you can explicitly select
|
||||
one by passing its name to the --container option
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local ssh
|
||||
$ balena local ssh --host
|
||||
$ balena local ssh --container chaotic_water
|
||||
$ balena local ssh --container chaotic_water --port 22222
|
||||
$ balena local ssh --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
localHostOSAccessOption,
|
||||
signature: 'container'
|
||||
parameter: 'container'
|
||||
default: null
|
||||
description: 'name of container to access'
|
||||
alias: 'c'
|
||||
,
|
||||
signature: 'port'
|
||||
parameter: 'port'
|
||||
description: 'ssh port number (default: 22222)'
|
||||
alias: 'p'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
child_process = require('child_process')
|
||||
Promise = require 'bluebird'
|
||||
_ = require('lodash')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if (options.host is true and options.container?)
|
||||
exitWithExpectedError('Please pass either --host or --container option')
|
||||
|
||||
if not options.port?
|
||||
options.port = 22222
|
||||
|
||||
verbose = if options.verbose then '-vvv' else ''
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (deviceIp) ->
|
||||
_.assign(options, { deviceIp })
|
||||
|
||||
return if options.host
|
||||
|
||||
if not options.container?
|
||||
return selectContainerFromDevice(deviceIp)
|
||||
|
||||
return options.container
|
||||
.then (container) ->
|
||||
|
||||
command = "ssh \
|
||||
#{verbose} \
|
||||
-t \
|
||||
-p #{options.port} \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
root@#{options.deviceIp}"
|
||||
|
||||
if not options.host
|
||||
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
|
||||
dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
|
||||
command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
stdio: 'inherit'
|
||||
.nodeify(done)
|
@ -1,79 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# A function to reliably execute a command
|
||||
# in all supported operating systems, including
|
||||
# different Windows environments like `cmd.exe`
|
||||
# and `Cygwin` should be encapsulated in a
|
||||
# re-usable package.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local stop [deviceIp]'
|
||||
description: 'Stop a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local stop
|
||||
$ balena local stop --app-name myapp
|
||||
$ balena local stop --all
|
||||
$ balena local stop 192.168.1.10
|
||||
$ balena local stop 192.168.1.10 --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'all'
|
||||
boolean: true
|
||||
description: 'stop all containers'
|
||||
,
|
||||
signature: 'app-name'
|
||||
parameter: 'name'
|
||||
description: 'name of container to stop'
|
||||
alias: 'a'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
chalk = require('chalk')
|
||||
{ forms, config, BalenaLocalDockerUtils } = require('balena-sync')
|
||||
{ selectContainerFromDevice, filterOutSupervisorContainer } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
@docker = new BalenaLocalDockerUtils(@deviceIp)
|
||||
|
||||
if options.all
|
||||
# Only list running containers
|
||||
return @docker.docker.listContainersAsync(all: false)
|
||||
.filter(filterOutSupervisorContainer)
|
||||
.then (containers) =>
|
||||
Promise.map containers, ({ Names, Id }) =>
|
||||
console.log(chalk.yellow.bold("* Stopping container #{Names[0]}"))
|
||||
@docker.stopContainer(Id)
|
||||
|
||||
ymlConfig = config.load()
|
||||
@appName = options['app-name'] ? ymlConfig['local_balenaos']?['app-name']
|
||||
@docker.checkForRunningContainer(@appName)
|
||||
.then (isRunning) =>
|
||||
if not isRunning
|
||||
return selectContainerFromDevice(@deviceIp, true)
|
||||
|
||||
console.log(chalk.yellow.bold("* Stopping container #{@appName}"))
|
||||
return @appName
|
||||
.then (runningContainerName) =>
|
||||
@docker.stopContainer(runningContainerName)
|
@ -1,172 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
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;
|
||||
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 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:
|
||||
'Only show logs for a single service. This can be used in combination with --system',
|
||||
parameter: 'service',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'system',
|
||||
alias: 'S',
|
||||
boolean: true,
|
||||
description:
|
||||
'Only show system logs. This can be used in combination with --service.',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { serviceIdToName } = await import('../utils/cloud');
|
||||
const { displayDeviceLogs, displayLogObject } = await import(
|
||||
'../utils/device/logs'
|
||||
);
|
||||
const { validateIPAddress } = await import('../utils/validation');
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
const Logger = await import('../utils/logger');
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
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,
|
||||
options.service,
|
||||
);
|
||||
} else {
|
||||
displayLogObject(
|
||||
line,
|
||||
logger,
|
||||
options.system || false,
|
||||
options.service,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
options.service,
|
||||
);
|
||||
} else {
|
||||
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,55 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.set =
|
||||
signature: 'note <|note>'
|
||||
description: 'set a device note'
|
||||
help: '''
|
||||
Use this command to set or update a device note.
|
||||
|
||||
If note command isn't passed, the tool attempts to read from `stdin`.
|
||||
|
||||
To view the notes, use $ balena device <uuid>.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | balena note --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
signature: 'device'
|
||||
parameter: 'device'
|
||||
description: 'device uuid'
|
||||
alias: [ 'd', 'dev' ]
|
||||
required: 'You have to specify a device'
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if _.isEmpty(params.note)
|
||||
exitWithExpectedError('Missing note content')
|
||||
|
||||
balena.models.device.note(options.device, params.note)
|
||||
.nodeify(done)
|
@ -1,427 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
formatVersion = (v, isRecommended) ->
|
||||
result = "v#{v}"
|
||||
if isRecommended
|
||||
result += ' (recommended)'
|
||||
return result
|
||||
|
||||
resolveVersion = (deviceType, version) ->
|
||||
if version isnt 'menu'
|
||||
if version[0] == 'v'
|
||||
version = version.slice(1)
|
||||
return Promise.resolve(version)
|
||||
|
||||
form = require('resin-cli-form')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
balena.models.os.getSupportedVersions(deviceType)
|
||||
.then ({ versions, recommended }) ->
|
||||
choices = versions.map (v) ->
|
||||
value: v
|
||||
name: formatVersion(v, v is recommended)
|
||||
|
||||
return form.ask
|
||||
message: 'Select the OS version:'
|
||||
type: 'list'
|
||||
choices: choices
|
||||
default: recommended
|
||||
|
||||
exports.versions =
|
||||
signature: 'os versions <type>'
|
||||
description: 'show the available balenaOS versions for the given device type'
|
||||
help: '''
|
||||
Use this command to show the available balenaOS versions for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os versions raspberrypi3
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
balena.models.os.getSupportedVersions(params.type)
|
||||
.then ({ versions, recommended }) ->
|
||||
versions.forEach (v) ->
|
||||
console.log(formatVersion(v, v is recommended))
|
||||
|
||||
exports.download =
|
||||
signature: 'os download <type>'
|
||||
description: 'download an unconfigured os image'
|
||||
help: '''
|
||||
Use this command to download an unconfigured os image for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded if available, or the newest version otherwise (if all existing
|
||||
versions for the given device type are pre-release).
|
||||
|
||||
You can pass `--version menu` to pick the OS version from the interactive menu
|
||||
of all available versions.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output path'
|
||||
parameter: 'output'
|
||||
alias: 'o'
|
||||
required: 'You have to specify the output location'
|
||||
}
|
||||
commandOptions.osVersionOrSemver
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
unzip = require('unzip2')
|
||||
fs = require('fs')
|
||||
rindle = require('rindle')
|
||||
manager = require('balena-image-manager')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
console.info("Getting device operating system for #{params.type}")
|
||||
|
||||
displayVersion = ''
|
||||
Promise.try ->
|
||||
if not options.version
|
||||
console.warn('OS version is not specified, using the default version:
|
||||
the newest stable (non-pre-release) version if available,
|
||||
or the newest version otherwise (if all existing
|
||||
versions for the given device type are pre-release).')
|
||||
return 'default'
|
||||
return resolveVersion(params.type, options.version)
|
||||
.then (version) ->
|
||||
if version isnt 'default'
|
||||
displayVersion = " #{version}"
|
||||
return manager.get(params.type, version)
|
||||
.then (stream) ->
|
||||
bar = new visuals.Progress("Downloading Device OS#{displayVersion}")
|
||||
spinner = new visuals.Spinner("Downloading Device OS#{displayVersion} (size unknown)")
|
||||
|
||||
stream.on 'progress', (state) ->
|
||||
if state?
|
||||
bar.update(state)
|
||||
else
|
||||
spinner.start()
|
||||
|
||||
stream.on 'end', ->
|
||||
spinner.stop()
|
||||
|
||||
# We completely rely on the `mime` custom property
|
||||
# to make this decision.
|
||||
# The actual stream should be checked instead.
|
||||
if stream.mime is 'application/zip'
|
||||
output = unzip.Extract(path: options.output)
|
||||
else
|
||||
output = fs.createWriteStream(options.output)
|
||||
|
||||
return rindle.wait(stream.pipe(output)).return(options.output)
|
||||
.tap (output) ->
|
||||
console.info('The image was downloaded successfully')
|
||||
.nodeify(done)
|
||||
|
||||
buildConfigForDeviceType = (deviceType, advanced = false) ->
|
||||
form = require('resin-cli-form')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
questions = deviceType.options
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
|
||||
return form.run(questions, { override })
|
||||
|
||||
buildConfig = (image, deviceTypeSlug, advanced = false) ->
|
||||
Promise = require('bluebird')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
Promise.resolve(helpers.getManifest(image, deviceTypeSlug))
|
||||
.then (deviceTypeManifest) ->
|
||||
buildConfigForDeviceType(deviceTypeManifest, advanced)
|
||||
|
||||
exports.buildConfig =
|
||||
signature: 'os build-config <image> <device-type>'
|
||||
description: 'build the OS config and save it to the JSON file'
|
||||
help: '''
|
||||
Use this command to prebuild the OS config once and skip the interactive part of `balena os configure`.
|
||||
|
||||
Example:
|
||||
|
||||
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.advancedConfig
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'the path to the output JSON file'
|
||||
alias: 'o'
|
||||
required: 'the output path is required'
|
||||
parameter: 'output'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(fs.writeFile)
|
||||
|
||||
buildConfig(params.image, params['device-type'], options.advanced)
|
||||
.then (answers) ->
|
||||
writeFileAsync(options.output, JSON.stringify(answers, null, 4))
|
||||
.nodeify(done)
|
||||
|
||||
exports.configure =
|
||||
signature: 'os configure <image>'
|
||||
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.
|
||||
|
||||
This command will try to automatically determine the operating system version in order
|
||||
to correctly configure the image. It may fail to do so however, in which case you'll
|
||||
have to call this command again with the exact version number of the targeted image.
|
||||
|
||||
Note that device api keys are only supported on balenaOS 2.0.3+.
|
||||
|
||||
This command still supports the *deprecated* format where the UUID and optionally device key
|
||||
are passed directly on the command line, but the recommended way is to pass either an --app or
|
||||
--device argument. The deprecated format will be removed in a future release.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena 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
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.advancedConfig
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
commandOptions.optionalOsVersion
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(fs.readFile)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
|
||||
if _.filter([
|
||||
options.device
|
||||
options.application
|
||||
]).length != 1
|
||||
patterns.exitWithExpectedError '''
|
||||
To configure an image, you must provide exactly one of:
|
||||
|
||||
* A device, with --device <uuid>
|
||||
* An application, with --app <appname>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help os configure
|
||||
'''
|
||||
|
||||
if !options.application and options.deviceType
|
||||
patterns.exitWithExpectedError '''
|
||||
Specifying a different device type is only supported when
|
||||
configuring an image using an application as a parameter:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help os configure
|
||||
'''
|
||||
|
||||
uuid = options.device
|
||||
deviceApiKey = options.deviceApiKey
|
||||
|
||||
console.info('Configuring operating system image')
|
||||
|
||||
configurationResourceType = if uuid then 'device' else 'application'
|
||||
|
||||
balena.models[configurationResourceType].get(uuid || options.application)
|
||||
.then (appOrDevice) ->
|
||||
deviceType = options.deviceType || appOrDevice.device_type
|
||||
manifestPromise = helpers.getManifest(params.image, deviceType)
|
||||
|
||||
if options.application && options.deviceType
|
||||
app = appOrDevice
|
||||
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
|
||||
paramManifestPromise = balena.models.device.getManifestBySlug(options.deviceType)
|
||||
manifestPromise = Promise.resolve(manifestPromise).tap ->
|
||||
Promise.join appManifestPromise, paramManifestPromise, (appDeviceType, paramDeviceType) ->
|
||||
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
"Device type #{options.deviceType} is incompatible with application #{options.application}"
|
||||
)
|
||||
|
||||
answersPromise = Promise.try ->
|
||||
if options.config
|
||||
return readFileAsync(options.config, 'utf8')
|
||||
.then(JSON.parse)
|
||||
return manifestPromise.then (deviceTypeManifest) ->
|
||||
buildConfigForDeviceType(deviceTypeManifest, options.advanced)
|
||||
|
||||
Promise.join answersPromise, manifestPromise, (answers, manifest) ->
|
||||
answers.version = options.version
|
||||
|
||||
if configurationResourceType == 'application'
|
||||
answers.deviceType = deviceType
|
||||
|
||||
if not answers.version?
|
||||
answers.version = Promise.resolve(helpers.getOsVersion(params.image, manifest)).tap (version) ->
|
||||
if not version?
|
||||
throw new Error(
|
||||
'Could not read OS version from the image. ' +
|
||||
'Please specify the version manually with the ' +
|
||||
'--version argument to this command.'
|
||||
)
|
||||
|
||||
Promise.props(answers).then (answers) ->
|
||||
(if configurationResourceType == 'device'
|
||||
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
|
||||
else
|
||||
generateApplicationConfig(appOrDevice, answers)
|
||||
)
|
||||
.then (config) ->
|
||||
init.configure(params.image, manifest, 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.
|
||||
'''
|
||||
|
||||
exports.initialize =
|
||||
signature: 'os initialize <image>'
|
||||
description: 'initialize an os image'
|
||||
help: """
|
||||
Use this command to initialize a device with previously configured operating system image.
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
"""
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.yes
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
commandOptions.drive
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
console.info("""
|
||||
Initializing device
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
""")
|
||||
Promise.resolve(helpers.getManifest(params.image, options.type))
|
||||
.then (manifest) ->
|
||||
return manifest.initialization?.options
|
||||
.then (questions) ->
|
||||
return form.run questions,
|
||||
override:
|
||||
drive: options.drive
|
||||
.tap (answers) ->
|
||||
return if not answers.drive?
|
||||
patterns.confirm(
|
||||
options.yes
|
||||
"This will erase #{answers.drive}. Are you sure?"
|
||||
"Going to erase #{answers.drive}."
|
||||
)
|
||||
.return(answers.drive)
|
||||
.then(umountAsync)
|
||||
.tap (answers) ->
|
||||
return helpers.sudo([
|
||||
'internal'
|
||||
'osinit'
|
||||
params.image
|
||||
options.type
|
||||
JSON.stringify(answers)
|
||||
])
|
||||
.then (answers) ->
|
||||
return if not answers.drive?
|
||||
|
||||
# TODO: balena local makes use of ejectAsync, see below
|
||||
# DO we need this / should we do that here?
|
||||
|
||||
# getDrive = (drive) ->
|
||||
# driveListAsync().then (drives) ->
|
||||
# selectedDrive = _.find(drives, device: drive)
|
||||
|
||||
# if not selectedDrive?
|
||||
# throw new Error("Drive not found: #{drive}")
|
||||
|
||||
# return selectedDrive
|
||||
# if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
# ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
# return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
umountAsync(answers.drive).tap ->
|
||||
console.info("You can safely remove #{answers.drive} now")
|
||||
.nodeify(done)
|
@ -1,306 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
allDeviceTypes = undefined
|
||||
|
||||
isCurrent = (commit) ->
|
||||
return commit == 'latest' or commit == 'current'
|
||||
|
||||
getDeviceTypes = ->
|
||||
Bluebird = require('bluebird')
|
||||
_ = require('lodash')
|
||||
if allDeviceTypes != undefined
|
||||
return Bluebird.resolve(allDeviceTypes)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.config.getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
_.sortBy(deviceTypes, 'name')
|
||||
.tap (dt) ->
|
||||
allDeviceTypes = dt
|
||||
|
||||
getDeviceTypesWithSameArch = (deviceTypeSlug) ->
|
||||
_ = require('lodash')
|
||||
|
||||
getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
deviceType = _.find(deviceTypes, slug: deviceTypeSlug)
|
||||
_(deviceTypes).filter(arch: deviceType.arch).map('slug').value()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds = (deviceType) ->
|
||||
preload = require('balena-preload')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
getDeviceTypesWithSameArch(deviceType)
|
||||
.then (deviceTypes) ->
|
||||
balena.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
$filter:
|
||||
device_type:
|
||||
$in: deviceTypes
|
||||
owns__release:
|
||||
$any:
|
||||
$alias: 'r'
|
||||
$expr:
|
||||
r:
|
||||
status: 'success'
|
||||
$expand: preload.applicationExpandOptions
|
||||
$select: [ 'id', 'app_name', 'device_type', 'commit', 'should_track_latest_release' ]
|
||||
$orderby: 'app_name asc'
|
||||
|
||||
selectApplication = (deviceType) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
|
||||
applicationInfoSpinner.start()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds(deviceType)
|
||||
.then (applications) ->
|
||||
applicationInfoSpinner.stop()
|
||||
if applications.length == 0
|
||||
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
|
||||
form.ask
|
||||
message: 'Select an application'
|
||||
type: 'list'
|
||||
choices: applications.map (app) ->
|
||||
name: app.app_name
|
||||
value: app
|
||||
|
||||
selectApplicationCommit = (releases) ->
|
||||
form = require('resin-cli-form')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if releases.length == 0
|
||||
exitWithExpectedError('This application has no successful releases.')
|
||||
DEFAULT_CHOICE = { 'name': 'current', 'value': 'current' }
|
||||
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
|
||||
name: "#{release.end_timestamp} - #{release.commit}"
|
||||
value: release.commit
|
||||
return form.ask
|
||||
message: 'Select a release'
|
||||
type: 'list'
|
||||
default: 'current'
|
||||
choices: choices
|
||||
|
||||
offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
if isCurrent(commit) or not application.should_track_latest_release or pinDevice
|
||||
return Promise.resolve()
|
||||
message = '''
|
||||
|
||||
This application is set to automatically update all devices to the current version.
|
||||
This might be unexpected behaviour: with this enabled, the preloaded device will still
|
||||
download and install the current release once it is online.
|
||||
|
||||
Do you want to disable automatic updates for this application?
|
||||
|
||||
Warning: To re-enable this requires direct api calls,
|
||||
see https://balena.io/docs/reference/api/resources/device/#set-device-to-release
|
||||
|
||||
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.
|
||||
'''
|
||||
form.ask
|
||||
message: message,
|
||||
type: 'confirm'
|
||||
.then (update) ->
|
||||
if not update
|
||||
return
|
||||
balena.pine.patch
|
||||
resource: 'application'
|
||||
id: application.id
|
||||
body:
|
||||
should_track_latest_release: false
|
||||
|
||||
module.exports =
|
||||
signature: 'preload <image>'
|
||||
description: 'preload an app on a disk image (or Edison zip archive)'
|
||||
help: '''
|
||||
Warning: "balena 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/balena-io/balena-cli .
|
||||
|
||||
Use this command to preload an application to a local disk image (or
|
||||
Edison zip archive) with a built release from balena.
|
||||
|
||||
Examples:
|
||||
|
||||
$ 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: '''
|
||||
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 the CLI to set the commit field on the application.
|
||||
'''
|
||||
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'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
preload = require('balena-preload')
|
||||
visuals = require('resin-cli-visuals')
|
||||
nodeCleanup = require('node-cleanup')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
progressBars = {}
|
||||
|
||||
progressHandler = (event) ->
|
||||
progressBar = progressBars[event.name]
|
||||
if not progressBar
|
||||
progressBar = progressBars[event.name] = new visuals.Progress(event.name)
|
||||
progressBar.update(percentage: event.percentage)
|
||||
|
||||
spinners = {}
|
||||
|
||||
spinnerHandler = (event) ->
|
||||
spinner = spinners[event.name]
|
||||
if not spinner
|
||||
spinner = spinners[event.name] = new visuals.Spinner(event.name)
|
||||
if event.action == 'start'
|
||||
spinner.start()
|
||||
else
|
||||
console.log()
|
||||
spinner.stop()
|
||||
|
||||
options.image = params.image
|
||||
options.appId = options.app
|
||||
delete options.app
|
||||
|
||||
options.splashImage = options['splash-image']
|
||||
delete options['splash-image']
|
||||
|
||||
options.dontCheckArch = options['dont-check-arch'] || false
|
||||
delete options['dont-check-arch']
|
||||
if options.dontCheckArch and not options.appId
|
||||
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
|
||||
|
||||
options.pinDevice = options['pin-device-to-release'] || false
|
||||
delete options['pin-device-to-release']
|
||||
|
||||
# Get a configured dockerode instance
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
|
||||
preloader = new preload.Preloader(
|
||||
balena
|
||||
docker
|
||||
options.appId
|
||||
options.commit
|
||||
options.image
|
||||
options.splashImage
|
||||
options.proxy
|
||||
options.dontCheckArch
|
||||
options.pinDevice
|
||||
)
|
||||
|
||||
gotSignal = false
|
||||
|
||||
nodeCleanup (exitCode, signal) ->
|
||||
if signal
|
||||
gotSignal = true
|
||||
nodeCleanup.uninstall() # don't call cleanup handler again
|
||||
preloader.cleanup()
|
||||
.then ->
|
||||
# calling process.exit() won't inform parent process of signal
|
||||
process.kill(process.pid, signal)
|
||||
return false
|
||||
|
||||
if process.env.DEBUG
|
||||
preloader.stderr.pipe(process.stderr)
|
||||
|
||||
preloader.on('progress', progressHandler)
|
||||
preloader.on('spinner', spinnerHandler)
|
||||
|
||||
return new Promise (resolve, reject) ->
|
||||
preloader.on('error', reject)
|
||||
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
# If no appId was provided, show a list of matching apps
|
||||
Promise.try ->
|
||||
if not preloader.appId
|
||||
selectApplication(preloader.config.deviceType)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
.then ->
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if isCurrent(options.commit) and preloader.application.commit
|
||||
# handle `--commit current` (and its `--commit latest` synonym)
|
||||
return 'latest'
|
||||
release = _.find preloader.application.owns__release, (release) ->
|
||||
release.commit.startsWith(options.commit)
|
||||
if not release
|
||||
exitWithExpectedError('There is no release matching this commit')
|
||||
return release.commit
|
||||
selectApplicationCommit(preloader.application.owns__release)
|
||||
.then (commit) ->
|
||||
if isCurrent(commit)
|
||||
preloader.commit = preloader.application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Propose to disable automatic app updates if the commit is not the current release
|
||||
offerToDisableAutomaticUpdates(preloader.application, commit, options.pinDevice)
|
||||
.then ->
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(balena.errors.BalenaError, exitWithExpectedError)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(done)
|
||||
.finally ->
|
||||
if not gotSignal
|
||||
preloader.cleanup()
|
@ -1,347 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2018 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
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;
|
||||
live: boolean;
|
||||
detached: boolean;
|
||||
service: string;
|
||||
system: boolean;
|
||||
}
|
||||
> = {
|
||||
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.
|
||||
|
||||
It is also possible to run a push to a local mode device in live mode.
|
||||
This will watch for changes in the source directory and perform an
|
||||
in-place build in the running containers [BETA].
|
||||
|
||||
${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 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`,
|
||||
},
|
||||
{
|
||||
signature: 'live',
|
||||
alias: 'l',
|
||||
boolean: true,
|
||||
description: stripIndent`
|
||||
Note this feature is in beta.
|
||||
|
||||
Start a live session with the containers pushed to a local mode device.
|
||||
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.`,
|
||||
},
|
||||
{
|
||||
signature: 'detached',
|
||||
alias: 'd',
|
||||
description: `Don't tail application logs when pushing to a local mode device`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'service',
|
||||
description: stripIndent`
|
||||
Only show logs from a single service. This can be used in combination with --system.
|
||||
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,
|
||||
},
|
||||
],
|
||||
async action(params, options, done) {
|
||||
const sdk = (await import('balena-sdk')).fromSharedOptions();
|
||||
const Bluebird = await import('bluebird');
|
||||
const remote = await import('../utils/remote-build');
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const { validateSpecifiedDockerfile, parseRegistrySecrets } = 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.log(`[debug] Using ${source} as build source`);
|
||||
}
|
||||
|
||||
const dockerfilePath = validateSpecifiedDockerfile(
|
||||
source,
|
||||
options.dockerfile,
|
||||
);
|
||||
|
||||
const registrySecrets = options['registry-secrets']
|
||||
? await parseRegistrySecrets(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.live) {
|
||||
exitWithExpectedError(
|
||||
'The --live flag is only valid when pushing to a local mode device',
|
||||
);
|
||||
}
|
||||
if (options.detached) {
|
||||
exitWithExpectedError(
|
||||
`The --detached 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.',
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
nocache: options.nocache,
|
||||
registrySecrets,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
|
||||
return await remote.startRemoteBuild(args);
|
||||
},
|
||||
).nodeify(done);
|
||||
break;
|
||||
case BuildTarget.Device:
|
||||
const device = appOrDevice;
|
||||
// TODO: Support passing a different port
|
||||
await Bluebird.resolve(
|
||||
deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
nocache: options.nocache || false,
|
||||
live: options.live || false,
|
||||
detached: options.detached || false,
|
||||
service: options.service,
|
||||
system: options.system || false,
|
||||
}),
|
||||
)
|
||||
.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,39 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
|
||||
export const list: CommandDefinition = {
|
||||
signature: 'settings',
|
||||
description: 'print current settings',
|
||||
help: `\
|
||||
Use this command to display detected settings
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena settings\
|
||||
`,
|
||||
async action(_params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const prettyjson = await import('prettyjson');
|
||||
|
||||
return balena.settings
|
||||
.getAll()
|
||||
.then(prettyjson.render)
|
||||
.then(console.log)
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
@ -1,149 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
module.exports =
|
||||
signature: 'ssh [uuid]'
|
||||
description: 'get a shell into the running app container of a device'
|
||||
help: '''
|
||||
Warning: 'balena ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check the README here: https://github.com/balena-io/balena-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena ssh MyApp
|
||||
$ balena ssh 7cf02a6
|
||||
$ balena ssh 7cf02a6 --port 8080
|
||||
$ balena ssh 7cf02a6 -v
|
||||
$ balena ssh 7cf02a6 -s
|
||||
$ balena ssh 7cf02a6 --noninteractive
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'port'
|
||||
parameter: 'port'
|
||||
description: 'ssh gateway port'
|
||||
alias: 'p'
|
||||
,
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
commandOptions.hostOSAccess,
|
||||
signature: 'noproxy'
|
||||
boolean: true
|
||||
description: "don't use the proxy configuration for this connection.
|
||||
Only makes sense if you've configured proxy globally."
|
||||
,
|
||||
signature: 'noninteractive'
|
||||
boolean: true
|
||||
description: 'run command non-interactively, do not automatically suggest devices to connect to if UUID not found'
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
child_process = require('child_process')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
_ = 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 `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.
|
||||
|
||||
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 balena.models.device.has(params.uuid)
|
||||
.then (uuidExists) ->
|
||||
return params.uuid if uuidExists
|
||||
if options.noninteractive
|
||||
console.error("Could not find device: #{params.uuid}")
|
||||
process.exit(1)
|
||||
return patterns.inferOrSelectDevice()
|
||||
.then (uuid) ->
|
||||
console.info("Connecting to: #{uuid}")
|
||||
balena.models.device.get(uuid)
|
||||
.then (device) ->
|
||||
patterns.exitWithExpectedError('Device is not online') if not device.is_online
|
||||
|
||||
Promise.props
|
||||
username: balena.auth.whoami()
|
||||
uuid: device.uuid
|
||||
# get full uuid
|
||||
containerId: if options.host then '' else balena.models.device.getApplicationInfo(device.uuid).get('containerId')
|
||||
proxyUrl: balena.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)
|
||||
|
||||
if options.host
|
||||
accessCommand = "host #{uuid}"
|
||||
else
|
||||
accessCommand = "enter #{uuid} #{containerId}"
|
||||
|
||||
command = "ssh #{verbose} -t \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
#{sshProxyCommand} \
|
||||
-p #{options.port} #{username}@ssh.#{proxyUrl} #{accessCommand}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
stdio: 'inherit'
|
||||
.nodeify(done)
|
@ -1,44 +0,0 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as BalenaSync from 'balena-sync';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export = deprecateSyncCmd(BalenaSync.capitano('balena-cli'));
|
||||
|
||||
const deprecationMsg = stripIndent`\
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Deprecation notice: please note that \`balena sync\` is deprecated and will
|
||||
be removed in a future release of the CLI. We are working on an exciting
|
||||
replacement that will be released soon!
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
`;
|
||||
|
||||
function deprecateSyncCmd(syncCmd: CommandDefinition): CommandDefinition {
|
||||
syncCmd.primary = false;
|
||||
syncCmd.description = syncCmd.description.replace(
|
||||
'(beta)',
|
||||
'[deprecated: see "help sync"]',
|
||||
);
|
||||
syncCmd.help = deprecationMsg + '\n\n' + syncCmd.help;
|
||||
const originalAction = syncCmd.action;
|
||||
syncCmd.action = (params, options, done): void => {
|
||||
console.log(deprecationMsg);
|
||||
originalAction(params, options, done);
|
||||
};
|
||||
return syncCmd;
|
||||
}
|
@ -1,277 +0,0 @@
|
||||
/*
|
||||
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 { normalizeUuidProp } from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
export const list: CommandDefinition<
|
||||
{},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: number;
|
||||
}
|
||||
> = {
|
||||
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
|
||||
`,
|
||||
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[]>(() => {
|
||||
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>
|
||||
|
||||
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) {
|
||||
return balena.models.release.tags.getAllByRelease(options.release);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
> = {
|
||||
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 myTagValue --release 1234
|
||||
$ balena tag set myCompositeTag "my tag value with whitespaces" --release 1234
|
||||
`,
|
||||
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(() => {
|
||||
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>
|
||||
|
||||
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) {
|
||||
return balena.models.release.tags.set(
|
||||
options.release,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
||||
|
||||
export const remove: CommandDefinition<
|
||||
{
|
||||
tagKey: string;
|
||||
},
|
||||
{
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: number;
|
||||
}
|
||||
> = {
|
||||
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
|
||||
`,
|
||||
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(() => {
|
||||
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>
|
||||
|
||||
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) {
|
||||
return balena.models.release.tags.remove(
|
||||
options.release,
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,245 +0,0 @@
|
||||
/*
|
||||
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 { inferOrSelectDevice } from '../utils/patterns';
|
||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
||||
|
||||
interface Args {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
port: string | string[];
|
||||
}
|
||||
|
||||
class DeviceIsOfflineError extends Error {
|
||||
public uuid: string;
|
||||
constructor(uuid: string) {
|
||||
super(`Device '${uuid}' is offline`);
|
||||
this.uuid = uuid;
|
||||
}
|
||||
}
|
||||
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 <uuid>',
|
||||
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,
|
||||
|
||||
async action(params, options, done) {
|
||||
const Logger = await import('../utils/logger');
|
||||
const logger = new Logger();
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
logger.logInfo(`Tunnel to ${params.uuid}`);
|
||||
|
||||
if (options.port === undefined) {
|
||||
throw new NoPortsDefinedError();
|
||||
}
|
||||
|
||||
const ports =
|
||||
typeof options.port !== 'string' && isArray(options.port)
|
||||
? (options.port as string[])
|
||||
: [options.port as string];
|
||||
|
||||
return inferOrSelectDevice(params.uuid)
|
||||
.then(sdk.models.device.get)
|
||||
.then(device => {
|
||||
if (!device.is_online) {
|
||||
throw new DeviceIsOfflineError(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,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
})
|
||||
.catch(err =>
|
||||
logConnection(
|
||||
client.remoteAddress,
|
||||
client.remotePort,
|
||||
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();
|
||||
|
||||
return Bluebird.all(localListeners);
|
||||
})
|
||||
.then(results => {
|
||||
if (!results.includes(true)) {
|
||||
throw new Error('No ports are valid for tunnelling');
|
||||
}
|
||||
|
||||
logger.logInfo('Waiting for connections...');
|
||||
})
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
@ -1,65 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
},
|
||||
};
|
229
lib/app.coffee
229
lib/app.coffee
@ -1,229 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
Raven = require('raven')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config require('./config').sentryDsn,
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: 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 'balena-sdk', to prevent any module from reading the http proxy config
|
||||
# before us
|
||||
globalTunnel = require('global-tunnel-ng')
|
||||
settings = require('balena-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/balena-io/capitano/issues/48
|
||||
global.PROXY_CONFIG = globalTunnel.proxyConfig
|
||||
|
||||
Promise = require('bluebird')
|
||||
capitano = require('capitano')
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
|
||||
# We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
# options correctly so we can do safely in submodules
|
||||
BalenaSdk = require('balena-sdk')
|
||||
BalenaSdk.setSharedOptions(
|
||||
apiUrl: settings.get('apiUrl')
|
||||
imageMakerUrl: settings.get('imageMakerUrl')
|
||||
dataDirectory: settings.get('dataDirectory')
|
||||
retries: 2
|
||||
)
|
||||
|
||||
actions = require('./actions')
|
||||
errors = require('./errors')
|
||||
events = require('./events')
|
||||
update = require('./utils/update')
|
||||
{ exitIfNotLoggedIn } = require('./utils/patterns')
|
||||
|
||||
# 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) ->
|
||||
exitIfNotLoggedIn()
|
||||
.then(done, 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)
|
||||
|
||||
# ---------- 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.signup)
|
||||
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.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)
|
||||
|
||||
# ---------- 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.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.logs)
|
||||
|
||||
# ---------- Sync Module ----------
|
||||
capitano.command(actions.sync)
|
||||
|
||||
# ---------- Tunnel Module ----------
|
||||
capitano.command(actions.tunnel.tunnel)
|
||||
|
||||
# ---------- Preload Module ----------
|
||||
capitano.command(actions.preload)
|
||||
|
||||
# ---------- SSH Module ----------
|
||||
capitano.command(actions.ssh)
|
||||
|
||||
# ---------- Local balenaOS 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)
|
||||
capitano.command(actions.internal.scanDevices)
|
||||
capitano.command(actions.internal.sudo)
|
||||
|
||||
#------------ Local build and deploy -------
|
||||
capitano.command(actions.build)
|
||||
capitano.command(actions.deploy)
|
||||
|
||||
#------------ Push/remote builds -------
|
||||
capitano.command(actions.push.push)
|
||||
|
||||
#------------ Join/Leave -------
|
||||
capitano.command(actions.join.join)
|
||||
capitano.command(actions.leave.leave)
|
||||
|
||||
update.notify()
|
||||
|
||||
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)
|
188
lib/app.ts
Normal file
188
lib/app.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import {
|
||||
AppOptions,
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/error-reporting/quickstart/?platform=node
|
||||
*/
|
||||
export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
Sentry.configureScope((scope) => {
|
||||
scope.setExtras({
|
||||
is_pkg: !!(process as any).pkg,
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
});
|
||||
});
|
||||
return Sentry.getCurrentHub();
|
||||
});
|
||||
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||
}
|
||||
}
|
||||
|
||||
/** Setup balena-sdk options that are shared with imported packages */
|
||||
function setupBalenaSdkSharedOptions(settings: CliSettings) {
|
||||
const BalenaSdk = require('balena-sdk') as typeof import('balena-sdk');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get<string>('apiUrl'),
|
||||
dataDirectory: settings.get<string>('dataDirectory'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Addresses the console warning:
|
||||
* (node:49500) MaxListenersExceededWarning: Possible EventEmitter memory
|
||||
* leak detected. 11 error listeners added. Use emitter.setMaxListeners() to
|
||||
* increase limit
|
||||
*/
|
||||
export function setMaxListeners(maxListeners: number) {
|
||||
require('events').EventEmitter.defaultMaxListeners = maxListeners;
|
||||
}
|
||||
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
}
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
await checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
// Proxy setup should be done early on, before loading balena-sdk
|
||||
await (await import('./utils/proxy')).setupGlobalHttpProxy(settings);
|
||||
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
if (!process.env.BALENARC_OFFLINE_MODE) {
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||
// checkForNewReleasesIfNeeded may query the npm registry
|
||||
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
let isEEXIT = false;
|
||||
try {
|
||||
await CustomMain.run(command);
|
||||
} catch (error) {
|
||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||
// for example the `balena help` command.
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
isEEXIT = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (shouldFlush) {
|
||||
await import('@oclif/command/flush');
|
||||
}
|
||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
||||
// addition to calling it in the main `run()` function in this file.
|
||||
// If it is not called here as well, there is a process exit delay of
|
||||
// 1 second when the fast-boot2 cache is modified (1 second is the
|
||||
// default cache saving timeout). Try for example `balena help`.
|
||||
// I have found that, when oclif's `Error: EEXIT: 0` is caught in
|
||||
// the try/catch block above, execution does not get past the
|
||||
// Promise.all() call below, but I don't understand why.
|
||||
if (isEEXIT) {
|
||||
(await import('./fast-boot')).stop();
|
||||
}
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
|
||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
||||
try {
|
||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||
'./utils/bootstrap'
|
||||
);
|
||||
setOfflineModeEnvVars();
|
||||
normalizeEnvVars();
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
await init();
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
const args = await preparseArgs(cliArgs);
|
||||
await oclifRun(args, options);
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
try {
|
||||
(await import('./fast-boot')).stop();
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Stopping fast-boot: ${e}`);
|
||||
}
|
||||
}
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
###
|
||||
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.
|
||||
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 auth
|
||||
###
|
||||
|
||||
open = require('opn')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
server = require('./server')
|
||||
utils = require('./utils')
|
||||
|
||||
###*
|
||||
# @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 balena dashboard where the session token exchange will
|
||||
# take place.
|
||||
#
|
||||
# Once the the token is retrieved, it's automatically persisted.
|
||||
#
|
||||
# @fulfil {String} - session token
|
||||
# @returns {Promise}
|
||||
#
|
||||
# @example
|
||||
# auth.login().then (sessionToken) ->
|
||||
# console.log('I\'m logged in!')
|
||||
# console.log("My session token is: #{sessionToken}")
|
||||
###
|
||||
exports.login = ->
|
||||
options =
|
||||
port: 8989
|
||||
path: '/auth'
|
||||
|
||||
# Needs to be 127.0.0.1 not localhost, because the ip only is whitelisted
|
||||
# from mixed content warnings (as the target of a form in the result page)
|
||||
callbackUrl = "http://127.0.0.1:#{options.port}#{options.path}"
|
||||
return utils.getDashboardLoginURL(callbackUrl).then (loginUrl) ->
|
||||
|
||||
# Leave a bit of time for the
|
||||
# server to get up and runing
|
||||
setTimeout ->
|
||||
open(loginUrl, { wait: false })
|
||||
, 1000
|
||||
|
||||
return server.awaitForToken(options)
|
||||
.tap(balena.auth.loginWithToken)
|
66
lib/auth/index.ts
Normal file
66
lib/auth/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { LoginServer } from './server';
|
||||
|
||||
/**
|
||||
* @module auth
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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 balena dashboard where the session token exchange will
|
||||
* take place.
|
||||
*
|
||||
* Once the the token is retrieved, it's automatically persisted.
|
||||
*
|
||||
* @fulfil {String} - session token
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* auth.login().then (sessionToken) ->
|
||||
* console.log('I\'m logged in!')
|
||||
* console.log("My session token is: #{sessionToken}")
|
||||
*/
|
||||
export async function login({ host = '127.0.0.1', port = 0 }) {
|
||||
const utils = await import('./utils');
|
||||
|
||||
const loginServer = new LoginServer();
|
||||
const {
|
||||
host: actualHost,
|
||||
port: actualPort,
|
||||
urlPath,
|
||||
} = await loginServer.start({ host, port });
|
||||
|
||||
const callbackUrl = `http://${actualHost}:${actualPort}${urlPath}`;
|
||||
const loginUrl = await utils.getDashboardLoginURL(callbackUrl);
|
||||
|
||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||
const open = await import('open');
|
||||
await open(loginUrl, { wait: false });
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const token = await loginServer.awaitForToken();
|
||||
await balena.auth.loginWithToken(token);
|
||||
loginServer.shutdown();
|
||||
return token;
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Balena 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,7 +12,8 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/sad.png" inline>
|
||||
<h1>Something went wrong</h1>
|
||||
<p>You couldn't login to the balena CLI for some reason</p>
|
||||
<br>
|
||||
<p>The balena CLI login was not successful.</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://forums.balena.io/" class="button danger">Get help in our forums</a>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Balena 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,10 +12,9 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/happy.png" inline>
|
||||
<h1>Success!</h1>
|
||||
<p>You successfully logged in the balena CLI</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="<%= dashboardUrl %>" class="button normal">Go to the dashboard</a>
|
||||
<p>The balena CLI login was successful.</p>
|
||||
<p>You may now close this page and return to the command prompt.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,101 +0,0 @@
|
||||
###
|
||||
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.
|
||||
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.
|
||||
###
|
||||
|
||||
express = require('express')
|
||||
path = require('path')
|
||||
bodyParser = require('body-parser')
|
||||
Promise = require('bluebird')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
utils = require('./utils')
|
||||
|
||||
createServer = ({ port, isDev } = {}) ->
|
||||
app = express()
|
||||
app.use bodyParser.urlencoded
|
||||
extended: true
|
||||
|
||||
app.set('view engine', 'ejs')
|
||||
app.set('views', path.join(__dirname, 'pages'))
|
||||
|
||||
if isDev
|
||||
app.use(express.static(path.join(__dirname, 'pages', 'static')))
|
||||
|
||||
server = app.listen(port)
|
||||
|
||||
return { app, server }
|
||||
|
||||
###*
|
||||
# @summary Await for token
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
# @param {Object} options - options
|
||||
# @param {String} options.path - callback path
|
||||
# @param {Number} options.port - http port
|
||||
#
|
||||
# @example
|
||||
# server.awaitForToken
|
||||
# path: '/auth'
|
||||
# port: 9001
|
||||
# .then (token) ->
|
||||
# console.log(token)
|
||||
###
|
||||
exports.awaitForToken = (options) ->
|
||||
{ app, server } = createServer(port: options.port)
|
||||
|
||||
return new Promise (resolve, reject) ->
|
||||
closeServer = (errorMessage, successPayload) ->
|
||||
server.close ->
|
||||
if errorMessage
|
||||
reject(new Error(errorMessage))
|
||||
return
|
||||
|
||||
resolve(successPayload)
|
||||
|
||||
renderAndDone = ({ request, response, viewName, errorMessage, statusCode, token }) ->
|
||||
return getContext(viewName)
|
||||
.then (context) ->
|
||||
response.status(statusCode || 200).render(viewName, context)
|
||||
request.connection.destroy()
|
||||
closeServer(errorMessage, token)
|
||||
|
||||
app.post options.path, (request, response) ->
|
||||
token = request.body.token?.trim()
|
||||
|
||||
Promise.try ->
|
||||
if not token
|
||||
throw new Error('No token')
|
||||
return utils.loginIfTokenValid(token)
|
||||
.tap (loggedIn) ->
|
||||
if not loggedIn
|
||||
throw new Error('Invalid token')
|
||||
.then ->
|
||||
renderAndDone({ request, response, viewName: 'success', token })
|
||||
.catch (error) ->
|
||||
renderAndDone({
|
||||
request, response, viewName: 'error',
|
||||
statusCode: 401, errorMessage: error.message
|
||||
})
|
||||
|
||||
app.use (request, response) ->
|
||||
response.status(404).send('Not found')
|
||||
closeServer('Unknown path or verb')
|
||||
|
||||
exports.getContext = getContext = (viewName) ->
|
||||
if viewName is 'success'
|
||||
return Promise.props
|
||||
dashboardUrl: balena.settings.get('dashboardUrl')
|
||||
|
||||
return Promise.resolve({})
|
149
lib/auth/server.ts
Normal file
149
lib/auth/server.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as bodyParser from 'body-parser';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as express from 'express';
|
||||
import type { Socket } from 'net';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as utils from './utils';
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
export class LoginServer extends EventEmitter {
|
||||
protected expressApp: express.Express;
|
||||
protected server: import('net').Server;
|
||||
protected serverSockets: Socket[] = [];
|
||||
protected firstError: Error;
|
||||
protected token: string;
|
||||
|
||||
public readonly loginPath = '/auth';
|
||||
|
||||
/**
|
||||
* Start the HTTP server, listening on the given IP address and port number.
|
||||
* If the port number is 0, the OS will allocate a free port number.
|
||||
*/
|
||||
public async start({ host = '127.0.0.1', port = 0 } = {}): Promise<{
|
||||
host: string;
|
||||
port: number;
|
||||
urlPath: string;
|
||||
}> {
|
||||
this.once('error', (err: Error) => {
|
||||
this.firstError = err;
|
||||
});
|
||||
this.on('token', (token: string) => {
|
||||
this.token = token;
|
||||
});
|
||||
|
||||
const app = (this.expressApp = express());
|
||||
app.use(
|
||||
bodyParser.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'pages'));
|
||||
|
||||
this.server = await new Promise<import('net').Server>((resolve, reject) => {
|
||||
const callback = (err: Error) => {
|
||||
if (err) {
|
||||
this.emit('error', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(server);
|
||||
}
|
||||
};
|
||||
const server = app.listen(port, host, callback as any);
|
||||
server.on('connection', (socket) => this.serverSockets.push(socket));
|
||||
});
|
||||
|
||||
this.expressApp.post(this.loginPath, async (request, response) => {
|
||||
this.server.close(); // stop listening for new connections
|
||||
try {
|
||||
const token = request.body.token?.trim();
|
||||
if (!token) {
|
||||
throw new ExpectedError('No token');
|
||||
}
|
||||
const loggedIn = await utils.loginIfTokenValid(token);
|
||||
if (!loggedIn) {
|
||||
throw new ExpectedError('Invalid token');
|
||||
}
|
||||
this.emit('token', token);
|
||||
response.status(200).render('success');
|
||||
} catch (error) {
|
||||
this.emit('error', error);
|
||||
response.status(401).render('error');
|
||||
}
|
||||
});
|
||||
|
||||
this.expressApp.use((_request, response) => {
|
||||
this.server.close(); // stop listening for new connections
|
||||
this.emit('error', new Error('Unknown path or verb'));
|
||||
response.status(404).send('Not found');
|
||||
});
|
||||
|
||||
return this.getAddress();
|
||||
}
|
||||
|
||||
public getAddress(): { host: string; port: number; urlPath: string } {
|
||||
const info = this.server.address() as import('net').AddressInfo;
|
||||
return {
|
||||
host: info.address,
|
||||
port: info.port,
|
||||
urlPath: this.loginPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut the server down.
|
||||
* Call this method to avoid the process hanging in some situations.
|
||||
*/
|
||||
public shutdown() {
|
||||
// A Node.js `http.server` instance prevents the process from exiting for up to
|
||||
// 2 minutes (by default) if a client keeps a HTTP connection open, and regardless
|
||||
// of whether `server.close()` was called: the `server.close(callback)` callback
|
||||
// takes just as long to complete. Setting `server.timeout` to some value like
|
||||
// 3 seconds works, but then the CLI process hangs for "only" 3 seconds. Reducing
|
||||
// the timeout to 1 second may cause authentication failure if the laptop or CI
|
||||
// server are slow for any reason. The only reliable way around it seems to be to
|
||||
// explicitly unref the sockets, so the event loop stops waiting for it. See:
|
||||
// https://github.com/nodejs/node/issues/2642
|
||||
// https://github.com/nodejs/node-v0.x-archive/issues/9066
|
||||
//
|
||||
this.serverSockets.forEach((s) => s.unref());
|
||||
this.serverSockets.splice(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Await for the user to complete login through a web browser.
|
||||
* Resolve to the authentication token string.
|
||||
*
|
||||
* @return Promise that resolves to the authentication token string
|
||||
*/
|
||||
public async awaitForToken(): Promise<string> {
|
||||
if (this.firstError) {
|
||||
throw this.firstError;
|
||||
}
|
||||
if (this.token) {
|
||||
return this.token;
|
||||
}
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.on('error', reject);
|
||||
this.on('token', resolve);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
###
|
||||
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.
|
||||
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.
|
||||
###
|
||||
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
_ = require('lodash')
|
||||
url = require('url')
|
||||
Promise = require('bluebird')
|
||||
|
||||
###*
|
||||
# @summary Get dashboard CLI login URL
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
# @param {String} callbackUrl - callback url
|
||||
# @fulfil {String} - dashboard login url
|
||||
# @returns {Promise}
|
||||
#
|
||||
# @example
|
||||
# utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
|
||||
# console.log(url)
|
||||
###
|
||||
exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
|
||||
# Encode percentages signs from the escaped url
|
||||
# characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25')
|
||||
|
||||
balena.settings.get('dashboardUrl').then (dashboardUrl) ->
|
||||
return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}")
|
||||
|
||||
###*
|
||||
# @summary Log in using a token, but only if the token is valid
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
# @description
|
||||
# This function checks that the token is not only well-structured
|
||||
# but that it also authenticates with the server successfully.
|
||||
#
|
||||
# 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.loginIfTokenValid('...').then (loggedIn) ->
|
||||
# if loggedIn
|
||||
# console.log('Token is valid!')
|
||||
###
|
||||
exports.loginIfTokenValid = (token) ->
|
||||
if not token? or _.isEmpty(token.trim())
|
||||
return Promise.resolve(false)
|
||||
|
||||
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 balena.auth.loginWithToken(currentToken)
|
||||
else
|
||||
return balena.auth.logout()
|
76
lib/auth/utils.ts
Normal file
76
lib/auth/utils.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
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.
|
||||
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 { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* Get dashboard CLI login URL
|
||||
*
|
||||
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
|
||||
* @returns Dashboard login URL, e.g.:
|
||||
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
|
||||
*/
|
||||
export async function getDashboardLoginURL(
|
||||
callbackUrl: string,
|
||||
): Promise<string> {
|
||||
// Encode percentages signs from the escaped url
|
||||
// characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
|
||||
|
||||
const [{ URL }, dashboardUrl] = await Promise.all([
|
||||
import('url'),
|
||||
getBalenaSdk().settings.get('dashboardUrl'),
|
||||
]);
|
||||
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in using a token, but only if the token is valid.
|
||||
*
|
||||
* This function checks that the token is not only well-structured
|
||||
* but that it also authenticates with the server successfully.
|
||||
*
|
||||
* If authenticated, the token is persisted, if not then the previous
|
||||
* login state is restored.
|
||||
*
|
||||
* @param token - session token or api key
|
||||
* @returns whether the login was successful or not
|
||||
*/
|
||||
export async function loginIfTokenValid(token?: string): Promise<boolean> {
|
||||
token = (token || '').trim();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
let currentToken;
|
||||
try {
|
||||
currentToken = await balena.auth.getToken();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
await balena.auth.loginWithToken(token);
|
||||
const isLoggedIn = await balena.auth.isLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
if (currentToken != null) {
|
||||
await balena.auth.loginWithToken(currentToken);
|
||||
} else {
|
||||
await balena.auth.logout();
|
||||
}
|
||||
}
|
||||
return isLoggedIn;
|
||||
}
|
175
lib/command.ts
Normal file
175
lib/command.ts
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Command from '@oclif/command';
|
||||
import {
|
||||
InsufficientPrivilegesError,
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
* When set to true, command will be listed in `help`,
|
||||
* otherwise listed in `help --verbose` with secondary commands.
|
||||
*/
|
||||
public static primary = false;
|
||||
|
||||
/**
|
||||
* Require elevated privileges to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if executed without root on Mac/Linux
|
||||
* or if executed by non-Administrator on Windows.
|
||||
*/
|
||||
public static root = false;
|
||||
|
||||
/**
|
||||
* Require authentication to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is not already logged in.
|
||||
*/
|
||||
public static authenticated = false;
|
||||
|
||||
/**
|
||||
* Require an internet connection to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
|
||||
*/
|
||||
public static offlineCompatible = false;
|
||||
|
||||
/**
|
||||
* Accept piped input.
|
||||
* When set to true, command will read from stdin during init
|
||||
* and make contents available on member `stdin`.
|
||||
*/
|
||||
public static readStdin = false;
|
||||
|
||||
public stdin: string;
|
||||
|
||||
/**
|
||||
* Throw InsufficientPrivilegesError if not root on Mac/Linux
|
||||
* or non-Administrator on Windows.
|
||||
*
|
||||
* Called automatically if `root=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*/
|
||||
protected static async checkElevatedPrivileges() {
|
||||
const isElevated = await (await import('is-elevated'))();
|
||||
if (!isElevated) {
|
||||
throw new InsufficientPrivilegesError(
|
||||
'You need root/admin privileges to run this command',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotLoggedInError if not logged in.
|
||||
*
|
||||
* Called automatically if `authenticated=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotLoggedInError}
|
||||
*/
|
||||
public static async checkLoggedIn() {
|
||||
await (await import('./utils/patterns')).checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotLoggedInError if not logged in when condition true.
|
||||
*
|
||||
* @param {boolean} doCheck - will check if true.
|
||||
* @throws {NotLoggedInError}
|
||||
*/
|
||||
public static async checkLoggedInIf(doCheck: boolean) {
|
||||
if (doCheck) {
|
||||
await this.checkLoggedIn();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotAvailableInOfflineModeError if in offline mode.
|
||||
*
|
||||
* Called automatically if `onlineOnly=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotAvailableInOfflineModeError}
|
||||
*/
|
||||
public static checkNotUsingOfflineMode() {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||
This command requires an internet connection, and cannot be used in offline mode.
|
||||
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read stdin contents and make available to command.
|
||||
*
|
||||
* This approach could be improved in the future to automatically set argument
|
||||
* values from stdin based in configuration, minimising command implementation.
|
||||
*/
|
||||
protected async getStdin() {
|
||||
this.stdin = await (await import('get-stdin'))();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a logger instance.
|
||||
*/
|
||||
protected static async getLogger() {
|
||||
return (await import('./utils/logger')).getLogger();
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
const ctr = this.constructor as typeof BalenaCommand;
|
||||
|
||||
if (ctr.root) {
|
||||
await BalenaCommand.checkElevatedPrivileges();
|
||||
}
|
||||
|
||||
if (ctr.authenticated) {
|
||||
await BalenaCommand.checkLoggedIn();
|
||||
}
|
||||
|
||||
if (!ctr.offlineCompatible) {
|
||||
BalenaCommand.checkNotUsingOfflineMode();
|
||||
}
|
||||
|
||||
if (ctr.readStdin) {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
protected printTitle = output.printTitle;
|
||||
}
|
85
lib/commands/api-key/generate.ts
Normal file
85
lib/commands/api-key/generate.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
|
||||
Generate a new balenaCloud API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
`;
|
||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'api-key generate <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command cannot be run when logged in with an API key.
|
||||
Please login again with 'balena login' and select an alternative method.
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
}
|
||||
}
|
265
lib/commands/build.ts
Normal file
265
lib/commands/build.ts
Normal file
@ -0,0 +1,265 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import {
|
||||
buildArgDeprecation,
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
} from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export default class BuildCmd extends Command {
|
||||
public static description = `\
|
||||
Build a project locally.
|
||||
|
||||
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 specify either a fleet, or the device type and architecture.
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file, and if found,
|
||||
each service defined in the compose file will be built. If a compose file isn't
|
||||
found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
|
||||
specified with the \`--dockerfile\` option), and if no dockerfile is found, it
|
||||
will try to generate one.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena build --fleet myFleet',
|
||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'source',
|
||||
description: 'path of project source directory',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'build [source]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
arch: flags.string({
|
||||
description: 'the architecture to build for',
|
||||
char: 'A',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
description: 'the type of device this build is for',
|
||||
char: 'd',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
BuildCmd,
|
||||
);
|
||||
|
||||
await Command.checkLoggedInIf(!!options.fleet);
|
||||
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
const logger = await Command.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// `build` accepts `source` as a parameter, but compose expects it as an option
|
||||
options.source = params.source;
|
||||
delete params.source;
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
|
||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||
|
||||
try {
|
||||
await this.buildProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
arch: options.arch!,
|
||||
deviceType: options.deviceType!,
|
||||
buildEmulated: options.emulated,
|
||||
buildOpts,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.logError('Build failed.');
|
||||
throw err;
|
||||
}
|
||||
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Build succeeded!');
|
||||
}
|
||||
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||
) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
throw new ExpectedError(
|
||||
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate project directory
|
||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||
sdk,
|
||||
{
|
||||
dockerfilePath: opts.dockerfile,
|
||||
noParentCheck: opts['noparent-check'] || false,
|
||||
projectPath: opts.source || '.',
|
||||
registrySecretsPath: opts['registry-secrets'],
|
||||
},
|
||||
);
|
||||
|
||||
opts.dockerfile = dockerfilePath;
|
||||
opts['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||
if (opts.fleet) {
|
||||
const { getAppWithArch } = await import('../utils/helpers');
|
||||
const app = await getAppWithArch(opts.fleet);
|
||||
opts.arch = app.arch;
|
||||
opts.deviceType = app.is_for__device_type[0].slug;
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
protected async prepareBuild(options: FlagsDef) {
|
||||
const { getDocker, generateBuildOpts } = await import('../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
getDocker(options),
|
||||
generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
return {
|
||||
docker,
|
||||
buildOpts,
|
||||
composeOpts,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opts must be an object with the following keys:
|
||||
* app: the app this build is for (optional)
|
||||
* arch: the architecture to build for
|
||||
* deviceType: the device type to build for
|
||||
* buildEmulated
|
||||
* buildOpts: arguments to forward to docker build command
|
||||
*
|
||||
* @param {DockerToolbelt} docker
|
||||
* @param {Logger} logger
|
||||
* @param {ComposeOpts} composeOpts
|
||||
* @param opts
|
||||
*/
|
||||
protected async buildProject(
|
||||
docker: import('dockerode'),
|
||||
logger: import('../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app?: Application;
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: BuildOpts;
|
||||
},
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
undefined,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
if (
|
||||
appType != null &&
|
||||
project.descriptors.length > 1 &&
|
||||
!appType.supports_multicontainer
|
||||
) {
|
||||
logger.logWarn(
|
||||
'Target fleet does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.',
|
||||
);
|
||||
}
|
||||
|
||||
await buildProject({
|
||||
docker,
|
||||
logger,
|
||||
projectPath: project.path,
|
||||
projectName: project.name,
|
||||
composition: project.composition,
|
||||
arch: opts.arch,
|
||||
deviceType: opts.deviceType,
|
||||
emulated: opts.buildEmulated,
|
||||
buildOpts: opts.buildOpts,
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
||||
}
|
251
lib/commands/config/generate.ts
Normal file
251
lib/commands/config/generate.ts
Normal file
@ -0,0 +1,251 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo, devModeInfo } from '../../utils/messages';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
fleet?: string;
|
||||
dev?: boolean; // balenaOS development variant
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
'generate-device-api-key': boolean;
|
||||
output?: string;
|
||||
// Options for non-interactive configuration
|
||||
network?: string;
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
'provisioning-key-name'?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ConfigGenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a config.json file.
|
||||
|
||||
Generate a config.json file for a device or fleet.
|
||||
|
||||
The target balenaOS version must be specified with the --version option.
|
||||
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
To avoid interactive questions, specify a command line option for each question that
|
||||
would otherwise be asked.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
];
|
||||
|
||||
public static usage = 'config generate';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
version: flags.string({
|
||||
description: 'a balenaOS version',
|
||||
required: true,
|
||||
}),
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['fleet', 'provisioning-key-name'],
|
||||
},
|
||||
deviceApiKey: flags.string({
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
char: 'k',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
}),
|
||||
'generate-device-api-key': flags.boolean({
|
||||
description: 'generate a fresh device key for the device',
|
||||
}),
|
||||
output: flags.string({
|
||||
description: 'path of output file',
|
||||
char: 'o',
|
||||
}),
|
||||
// Options for non-interactive configuration
|
||||
network: flags.string({
|
||||
description: 'the network type to use: ethernet or wifi',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
wifiSsid: flags.string({
|
||||
description:
|
||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
wifiKey: flags.string({
|
||||
description:
|
||||
'the wifi key to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
appUpdatePollInterval: flags.string({
|
||||
description:
|
||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
||||
}),
|
||||
'provisioning-key-name': flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await this.validateOptions(options);
|
||||
|
||||
let resourceDeviceType: string;
|
||||
let application: ApplicationWithDeviceType | null = null;
|
||||
let device:
|
||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||
| null = null;
|
||||
if (options.device != null) {
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const rawDevice = await balena.models.device.get(
|
||||
tryAsInteger(options.device),
|
||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
||||
);
|
||||
if (!rawDevice.belongs_to__application) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(stripIndent`
|
||||
Device ${options.device} does not appear to belong to an accessible fleet.
|
||||
Try with a different device, or use '--fleet' instead of '--device'.`);
|
||||
}
|
||||
device = rawDevice as DeviceWithDeviceType & {
|
||||
belongs_to__application: PineDeferred;
|
||||
};
|
||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
application = (await getApplication(balena, options.fleet!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||
}
|
||||
|
||||
const deviceType = options.deviceType || resourceDeviceType;
|
||||
|
||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||
deviceType,
|
||||
);
|
||||
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.fleet && options.deviceType) {
|
||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||
resourceDeviceType,
|
||||
);
|
||||
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (
|
||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for values
|
||||
// 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)
|
||||
const answers = await getCliForm().run(deviceManifest.options, {
|
||||
override: { ...options, app: options.fleet, application: options.fleet },
|
||||
});
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
|
||||
// Generate config
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
|
||||
let config;
|
||||
if (device) {
|
||||
config = await generateDeviceConfig(
|
||||
device,
|
||||
options.deviceApiKey || options['generate-device-api-key'] || undefined,
|
||||
answers,
|
||||
);
|
||||
} else if (application) {
|
||||
answers.deviceType = deviceType;
|
||||
config = await generateApplicationConfig(application, answers);
|
||||
}
|
||||
|
||||
// Output
|
||||
if (options.output != null) {
|
||||
const fs = await import('fs');
|
||||
await fs.promises.writeFile(options.output, JSON.stringify(config));
|
||||
}
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(config));
|
||||
}
|
||||
|
||||
protected readonly missingDeviceOrAppMessage = stripIndent`
|
||||
Either a device or a fleet must be specified.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
`;
|
||||
|
||||
protected readonly deviceTypeNotAllowedMessage =
|
||||
'The --deviceType option can only be used alongside the --fleet option';
|
||||
|
||||
protected async validateOptions(options: FlagsDef) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
if (options.device == null && options.fleet == null) {
|
||||
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
||||
}
|
||||
|
||||
if (!options.fleet && options.deviceType) {
|
||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||
}
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, options.version);
|
||||
}
|
||||
}
|
89
lib/commands/config/inject.ts
Normal file
89
lib/commands/config/inject.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default class ConfigInjectCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Inject a config.json file to a balenaOS image or attached media.
|
||||
|
||||
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config inject my/config.json',
|
||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'file',
|
||||
description: 'the path to the config.json file to inject',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigInjectCmd,
|
||||
);
|
||||
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device/OS drive'));
|
||||
await safeUmount(drive);
|
||||
|
||||
const fs = await import('fs');
|
||||
const configJSON = JSON.parse(
|
||||
await fs.promises.readFile(params.file, 'utf8'),
|
||||
);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
await config.write(drive, '', configJSON);
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
}
|
78
lib/commands/config/read.ts
Normal file
78
lib/commands/config/read.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
|
||||
export default class ConfigReadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Read the config.json file of a balenaOS image or attached media.
|
||||
|
||||
Read the 'config.json' file of a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config read',
|
||||
'$ balena config read --drive /dev/disk2',
|
||||
'$ balena config read --drive balena.img',
|
||||
];
|
||||
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...cf.deviceTypeIgnored,
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, '');
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(configJSON, null, 4));
|
||||
} else {
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(configJSON));
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user