mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
835 Commits
Author | SHA1 | Date | |
---|---|---|---|
6c26e1235c | |||
bacca5383a | |||
32e72c832f | |||
05aaed07b2 | |||
7c750f9e43 | |||
55bf4dc0f0 | |||
0afbd6f17a | |||
66b997d98c | |||
d485fd00a0 | |||
3322faeeb2 | |||
c32d894e97 | |||
ad737b8e02 | |||
bcc86fbcb6 | |||
d5c7527f8d | |||
5df65f67c3 | |||
79e65025cb | |||
dff6dafe85 | |||
adcc862acb | |||
8bf884d425 | |||
a6b282598b | |||
77089e31e4 | |||
7c6bae491f | |||
d5586e12d4 | |||
7e1f4791ed | |||
9d5ecb5f9c | |||
236dce37da | |||
a2ee48f2fb | |||
b74a0d1141 | |||
34d7b84d1e | |||
d999b901bb | |||
4a9d133c11 | |||
3a7604368a | |||
df2e611c42 | |||
30e48b658f | |||
f095ac169a | |||
f0030a1891 | |||
1d3af3245a | |||
f4612116b9 | |||
65ab3008e6 | |||
36026d8556 | |||
436ad60f4e | |||
e0ee333717 | |||
3b09c5ac91 | |||
6994499f14 | |||
9e19b5875b | |||
c3e5147a19 | |||
5e46815ac7 | |||
7b37c60e11 | |||
cf9fdbe6e4 | |||
66dfddc96d | |||
1fa2347608 | |||
6bed43fe1f | |||
46806c8377 | |||
cf93438df1 | |||
ea43130135 | |||
5e4daf8c3d | |||
20474aeb55 | |||
825213c02a | |||
13cef01374 | |||
7271f90dc6 | |||
8b5ebe0645 | |||
24e49bf131 | |||
5a0ef354f1 | |||
f8a9c10a77 | |||
bace8b5c1e | |||
d8c942c77e | |||
7fccd4a35e | |||
b78dd26f23 | |||
47c63e0e1d | |||
5d137f3c20 | |||
2bbdfda92e | |||
2f2f16267f | |||
051268168a | |||
2b264df41b | |||
eba193278e | |||
462b41b4ea | |||
ab5815c277 | |||
f75ffb53f5 | |||
3387f8f656 | |||
e8325e8268 | |||
c2491497b5 | |||
4596005a1f | |||
8d9cbbb526 | |||
17e51799f5 | |||
df797cdc2c | |||
57fc26c0f7 | |||
abc2cfd14c | |||
612fefcc65 | |||
0bbe376e41 | |||
04223dbc58 | |||
b5c4348de1 | |||
751749325f | |||
1e2e48b149 | |||
01b454351b | |||
6696b1b5f7 | |||
5da307f02e | |||
b391c96e64 | |||
0ee73f5164 | |||
1a1861bfcb | |||
717c43f10b | |||
dafbdd5f34 | |||
c204dbd6cd | |||
6e7f51758e | |||
ea89a6f221 | |||
94c9e13106 | |||
64c2f00d2a | |||
8f8d6b5f08 | |||
c49a1d3fbf | |||
abf573fa47 | |||
13e3e5e8ea | |||
faa558b432 | |||
4eea5e822b | |||
fe3e348128 | |||
7535b7110d | |||
0aaf6dff41 | |||
aca58743ea | |||
f6a262bcde | |||
9cc81866eb | |||
0607c2f231 | |||
fe0ba62026 | |||
f2af7b2588 | |||
e145540132 | |||
d21b84956c | |||
841ce9fd68 | |||
a4efc7c9c4 | |||
e6ecb0ec0b | |||
c420d0f63c | |||
f3ef7f6e18 | |||
e36435bb4c | |||
825964fdc6 | |||
5202e137d5 | |||
d23d837b8c | |||
8c537c112d | |||
106b971410 | |||
39cf86ed85 | |||
5de7a50fc0 | |||
ba4301487f | |||
f77156772a | |||
a6d6035725 | |||
5e0d24a1f1 | |||
9434570c2d | |||
674c0ca7b8 | |||
cccc8012c9 | |||
29bfcf7ac5 | |||
2091768c84 | |||
36ab6f5808 | |||
b45e80654c | |||
8c60c9e076 | |||
d47fe0609f | |||
3b5f3c6665 | |||
1bfba85d58 | |||
cb14928866 | |||
4088e4c66e | |||
01a1bcdc8a | |||
05c3d2a5db | |||
7da250914e | |||
eaad5377b4 | |||
9f15ee58df | |||
ee267cd114 | |||
b35a51ef3a | |||
7ce43f4018 | |||
3ba8be02e7 | |||
9a9d3f5c32 | |||
0adaeb5465 | |||
783cab2e50 | |||
a47d2d4454 | |||
1f728050c8 | |||
15ec99577a | |||
fb2e498aa9 | |||
7529a9a2a2 | |||
22b02c261f | |||
8f014710c0 | |||
308d1afb83 | |||
c15276d239 | |||
ecae517de0 | |||
69cc2a0946 | |||
7a8fc14686 | |||
eaa886c31c | |||
20ae2bc57a | |||
96c975d17e | |||
ff8d784582 | |||
53bee83047 | |||
6e343c36a8 | |||
e29c275b4c | |||
7faf363180 | |||
a503cb4757 | |||
b3470ac909 | |||
ba4c93ccf5 | |||
87401ad569 | |||
181afb34f8 | |||
bfdfa28922 | |||
21840d9245 | |||
d9c3332cb2 | |||
29d684f9c3 | |||
a832f47508 | |||
4557cf626f | |||
8c68aaad49 | |||
260c6fccd2 | |||
d40f2eb500 | |||
b6f3975bc1 | |||
f2bd3c0ffb | |||
3ae01fdaa0 | |||
2452b42f81 | |||
3303ac21c9 | |||
1b277bda87 | |||
5f67c243c0 | |||
9bbfb31bf7 | |||
5b805fe1da | |||
24ed25aa37 | |||
2ad0b60aeb | |||
37bd6be77b | |||
88ad591a83 | |||
30c36a26e2 | |||
6d6afc5140 | |||
fc79d89f10 | |||
57fba32fa2 | |||
b41f9b9261 | |||
a9aa7538f3 | |||
1b13d1b969 | |||
e6b09f1b94 | |||
8fa592dff0 | |||
a6d2950260 | |||
b22ddb50f1 | |||
0aa10ba2a1 | |||
56c74af1ff | |||
6460d850ca | |||
b05aa7b385 | |||
97c15208b5 | |||
375464eb1a | |||
811262ed8b | |||
f816cb4ce8 | |||
7b5272e926 | |||
d412d39164 | |||
d41fb72ded | |||
4676396b5f | |||
b97565d2e7 | |||
a697121b97 | |||
12615cd0dc | |||
cba73eec44 | |||
f5ed0648ba | |||
ac5ffeda09 | |||
db25a65753 | |||
296f1ae2de | |||
579cdaa2e2 | |||
69db3c0171 | |||
7c71098d86 | |||
490f833a33 | |||
a81c1971f1 | |||
76034696e9 | |||
454f82883e | |||
6a9a9e1fdb | |||
cf2ad66955 | |||
4cfaf6e666 | |||
bc563ea963 | |||
65ac35a93e | |||
1ee51ca9a7 | |||
e9e15dbbe3 | |||
a665a3d153 | |||
9da5f88ecf | |||
14e9b34636 | |||
e619caea42 | |||
a133fe8c6f | |||
29dd5e71a1 | |||
9b52dec725 | |||
6bc55ea7ab | |||
717affa591 | |||
6a9eeaaba2 | |||
98eaeddbfe | |||
30698c62e3 | |||
79e240f630 | |||
6825ffe416 | |||
b9bf00d329 | |||
5ae7457f45 | |||
d78dfcb1de | |||
95c4c59ca0 | |||
3a06c5df72 | |||
d30144a16a | |||
c0990fe6c4 | |||
af382bfee4 | |||
6705369ca6 | |||
fb05957198 | |||
6b21f5aa5a | |||
0fac8d8d3b | |||
f39193ab61 | |||
a883948d56 | |||
da86d3303f | |||
2f3138208a | |||
e688e10684 | |||
66b62df70b | |||
a4dd45e6a6 | |||
b4439b7d78 | |||
bf566b7bb7 | |||
2c897a1b18 | |||
a5cfbb3181 | |||
119a630643 | |||
c6fe6b5e3e | |||
6ff43b11b1 | |||
f35655028e | |||
709af3e92b | |||
5ec9dce507 | |||
1e81638433 | |||
145b613f5d | |||
a243c3f577 | |||
75b9ba907f | |||
1a368ac4d4 | |||
2833e8ba23 | |||
de3837f777 | |||
8dc5eaca52 | |||
5c41de0c9d | |||
7a258f022f | |||
cbdf1c3ccf | |||
dcab2404fa | |||
05e80094de | |||
9fab994dec | |||
6eddd1ccd3 | |||
211fb824a1 | |||
17c7b97abe | |||
ac3c539d45 | |||
c1e94e661f | |||
8a6ee5905a | |||
36c636474d | |||
0bff122b1c | |||
2ffb9bb574 | |||
6190d00644 | |||
67673a55f7 | |||
4448509d92 | |||
8482961f7f | |||
552f8cc4ef | |||
21b32633c5 | |||
8863132e8e | |||
f72b556d92 | |||
4b7e0a19eb | |||
3db92322ba | |||
aac668dfca | |||
0636dcf19d | |||
3fca56e819 | |||
6124d8c493 | |||
9ef99a3aa9 | |||
66fc47edae | |||
af948e76f3 | |||
dfd98efe8b | |||
a8de833c43 | |||
3bff748fbe | |||
8adf66512b | |||
d4313e6f95 | |||
24fdfc9aef | |||
e5f454bac3 | |||
ca9ce5ed16 | |||
2087622bd6 | |||
a651e27a20 | |||
0fd0b6e1fd | |||
c63569d592 | |||
7b7d00c642 | |||
9e27889f91 | |||
8bbb1966a4 | |||
5d00e295fd | |||
0d4a2b65a0 | |||
2ba53649bd | |||
31f4af721d | |||
ce734ba783 | |||
77196746b3 | |||
99650ab732 | |||
96b7d4a15d | |||
ce1aff1557 | |||
9d5949e9d1 | |||
3ca681a4a6 | |||
49449e42be | |||
dad3167f16 | |||
3cc632fbbb | |||
f780d47198 | |||
e0bd6b9d4e | |||
5cf0f7030d | |||
77b763a88f | |||
f9390ceb10 | |||
bc41ff0540 | |||
54e91eb074 | |||
a42a1a97ba | |||
f3d5e26e1e | |||
99eae385b8 | |||
f6d67b94f3 | |||
2d9bb2130e | |||
10fff8f0f5 | |||
8ee994ce7d | |||
86aed2185d | |||
64ec151e4b | |||
3e4e661b28 | |||
7713ca31e5 | |||
b0da1b4811 | |||
0f302d30ec | |||
140e851fcd | |||
b9b4343fd5 | |||
eff49beb36 | |||
952d74207d | |||
853d146457 | |||
97d6a39677 | |||
095a597381 | |||
a66aec6965 | |||
03a3ef38e1 | |||
464d706920 | |||
61dd5acb80 | |||
1e5cf8655e | |||
f096f4f55f | |||
85442c4634 | |||
a357405f3a | |||
6070ee0f83 | |||
f8721a324d | |||
ca861a6349 | |||
493c6576c3 | |||
a8765af589 | |||
ca8484b466 | |||
7a8d746a54 | |||
1cffcd9b9e | |||
b6c041c9b5 | |||
47b35db03e | |||
78985ff633 | |||
93a5380c09 | |||
6677f1faf5 | |||
e7b32e941a | |||
5abd240d50 | |||
759c2d4a6f | |||
92772952fd | |||
2f53cbf088 | |||
c3b74a869a | |||
841d1927a9 | |||
06c450e9a5 | |||
67de638c76 | |||
c90b8eef97 | |||
6ad4598e7e | |||
fd580083d5 | |||
a0003c5f13 | |||
8291c96e69 | |||
561325e66d | |||
a840f39a91 | |||
64f9b50e40 | |||
0273d2e02c | |||
daf3b980ef | |||
6e36cd139a | |||
9ca76348ff | |||
58a5725ad2 | |||
116c3c787c | |||
74a896b3cd | |||
e2ebac27ea | |||
b799f3a46d | |||
3a3cfbc85e | |||
161b9454c2 | |||
b83b7145af | |||
26c4e466bd | |||
42f752e400 | |||
0b67a40d57 | |||
69ab9788fc | |||
7972187b77 | |||
a809847d60 | |||
203285bab9 | |||
52c7a098cc | |||
75bc937995 | |||
dd41145912 | |||
0983bf02e2 | |||
0deb59b6e2 | |||
fdc9fd67d8 | |||
01eb4b473d | |||
4ff42c11e6 | |||
85d82ab9ca | |||
dc6cde2cf1 | |||
ea1c1bb8d4 | |||
c6eca9f895 | |||
e71f622453 | |||
b6266878d4 | |||
4907fccf48 | |||
f4b84941cd | |||
c2df87bcc6 | |||
79f33c749b | |||
fd316167d8 | |||
f60d857c93 | |||
31628cfdcb | |||
4d42f74c0c | |||
13729ec4b6 | |||
8dc4c0871a | |||
207e080b9e | |||
39fe63fb2d | |||
24c2ffefc9 | |||
c293a1742d | |||
cb46756d31 | |||
332e731023 | |||
f9263975bc | |||
67ebf7aa19 | |||
2b52d5edbc | |||
948e6ea6f8 | |||
ca9247fb19 | |||
73455b4264 | |||
28b0793fc9 | |||
c904726259 | |||
6606b65c9b | |||
61160fd2f5 | |||
bf71f9ea16 | |||
fe751fdb23 | |||
947f91d570 | |||
c5d4e30e24 | |||
f560aa7523 | |||
6bcfb2dd51 | |||
bf062124f7 | |||
221666f59a | |||
4369a2d161 | |||
cd6ee4ef5e | |||
872b17cf24 | |||
88e11347bc | |||
a3dd489c70 | |||
0c1c108b2b | |||
f02ed43f33 | |||
63c3d7ceee | |||
dac45a884e | |||
ec589c2639 | |||
f65e777d1b | |||
684ac9fa24 | |||
330cbc6a68 | |||
14bfca8c3a | |||
20c07d31b2 | |||
64b4f67477 | |||
a8ceadc300 | |||
973d25f467 | |||
0d06701e2f | |||
379f1cc217 | |||
7b7ae4ff89 | |||
8e83a401eb | |||
2d1891a182 | |||
8df066df12 | |||
bd59f95e1a | |||
2b982a1c0c | |||
ab64fbc904 | |||
733b98f072 | |||
7c538a3658 | |||
8298ba5765 | |||
33a23773d8 | |||
21a3b82845 | |||
8688eb5da0 | |||
5b0ea9673f | |||
44fd8adeba | |||
a5e03d55c3 | |||
80629322ea | |||
946efbcb7f | |||
be8a314d2b | |||
0a7203cafe | |||
786fed0151 | |||
9cd8228a20 | |||
652b5f22dd | |||
eed3c06789 | |||
3b283d4a98 | |||
bc6b5ba7b3 | |||
74789ae88f | |||
295d6dee74 | |||
5010a1e312 | |||
3c2f7ea622 | |||
94f02f0ad8 | |||
375f84b24e | |||
06c649dfd0 | |||
71eca70a22 | |||
53c7bc622c | |||
975ae45e49 | |||
e7c68c1a5c | |||
5beeb78220 | |||
c90ba7aa0f | |||
802ccc1b9a | |||
b6ef251625 | |||
fd707d6a07 | |||
392cd8569f | |||
e32eda26d9 | |||
d8aaccf80c | |||
d5fd5f5f2d | |||
2cb69c12f1 | |||
7c75346a1a | |||
148d15b6d9 | |||
a46a79df59 | |||
e350f9b335 | |||
bd00773f1b | |||
ef3c7f0fd6 | |||
f4f44f978e | |||
442416efc3 | |||
ef33ffedcf | |||
430d4aeaa7 | |||
171632f83f | |||
1fa7141b58 | |||
916cc36430 | |||
27b877dd33 | |||
5cbe1c410f | |||
7846af390e | |||
79d9ebc805 | |||
25b853c535 | |||
a93141343f | |||
9a467c5ecd | |||
70be2ae596 | |||
36eb0a108e | |||
0bf6fb1739 | |||
892adf4c47 | |||
5d1d004b72 | |||
dea5a60b2d | |||
652a1b7650 | |||
350843af1e | |||
e04c4a8ee3 | |||
9d0c3f7535 | |||
9561d4da2e | |||
8296dcf946 | |||
e62e8b88c2 | |||
4388a248b9 | |||
f9cf0aaf23 | |||
dc9ee09838 | |||
7cb27283c5 | |||
10a9840b34 | |||
ce3e04bfe8 | |||
52f93f8f12 | |||
af9e1a122d | |||
9017b8ec11 | |||
bf4f687a2a | |||
9d4e6eb825 | |||
fba4afb7d2 | |||
8c74f784f7 | |||
69ca1ffa59 | |||
7d1b00877e | |||
1a48fed1f7 | |||
bc86359e63 | |||
f6822f1502 | |||
398c34d842 | |||
72a893be95 | |||
7b23b0e103 | |||
0ce7878042 | |||
da8483e6a6 | |||
16f70fd946 | |||
78aa898b37 | |||
b7f94a222d | |||
7bea2c26b8 | |||
7c178b8095 | |||
865f085094 | |||
28fe69fe94 | |||
232cf8d426 | |||
22e74983b0 | |||
c88dd2257a | |||
439d8d396f | |||
6d8086c09b | |||
e85f252f29 | |||
4b818ad51c | |||
c2518448a3 | |||
e7a8deed05 | |||
0ac599d20c | |||
7d7074e6b7 | |||
35ca34d07d | |||
90d7316b4c | |||
904b4e96d9 | |||
2c46c59a79 | |||
297ff86895 | |||
a154401424 | |||
ad2713fc00 | |||
6388cfaf40 | |||
167f38e342 | |||
919b3c3435 | |||
2e1ab22173 | |||
0a23563d7e | |||
37e4ec6364 | |||
6a8b947c2e | |||
a16ac37625 | |||
cf4c7826b2 | |||
a0a26f0a1e | |||
a921139a12 | |||
36da7b66c8 | |||
3aa87544eb | |||
6121fa505e | |||
a5ba5befd1 | |||
b7214a306c | |||
d7616e941a | |||
834a2f1e4d | |||
0e5f2fe748 | |||
e0bcb5e0b9 | |||
59d4890eae | |||
51da5360da | |||
2655aef28b | |||
45d3a7a124 | |||
662e4f8940 | |||
c06993cb8e | |||
a650f30ce8 | |||
0a924b2dcb | |||
89f62683ce | |||
143d88f3df | |||
d166a65422 | |||
dd268993b3 | |||
13a35b288f | |||
81e653d31b | |||
875ec8b8bd | |||
989df9b857 | |||
0829d3c176 | |||
ce64889b04 | |||
d3a0bfc5f6 | |||
e965c603d2 | |||
0e2fb8c96c | |||
2db1d84d3c | |||
12a1916007 | |||
b4526e9895 | |||
a2d867c860 | |||
05b1c37379 | |||
906cfe9268 | |||
3c8054faa7 | |||
c6c9046826 | |||
2bbbbf6fdd | |||
9cce4001af | |||
2e944cf2f4 | |||
2b0143775c | |||
49fec7d8f2 | |||
ca1ac2bb83 | |||
50b1a7e6b0 | |||
69ce2c0473 | |||
a3b446dbe7 | |||
1032d9927f | |||
12e8a50abc | |||
a4142097f8 | |||
b388ccb6f3 | |||
e011502b7e | |||
4f167cb836 | |||
9455d438e2 | |||
a356ecf9b6 | |||
066ac591ac | |||
62f006b89a | |||
ee75ff2753 | |||
e4c9defb70 | |||
bb102c1918 | |||
24ebe2946c | |||
ba82b1fa27 | |||
e3b145e7b7 | |||
242c3731ee | |||
5f7eee8eac | |||
1833f6ff0a | |||
e5fb954645 | |||
13f76dc020 | |||
b409bdcc73 | |||
8c3cb3f585 | |||
76a8b4df50 | |||
a03680311d | |||
6ee36cb5c7 | |||
5625326c65 | |||
b912419839 | |||
fe01ead023 | |||
229c105d0c | |||
b6e044345f | |||
d9906121e1 | |||
3e019f7f34 | |||
eb34cb6f27 | |||
3a3178bcb9 | |||
cdf6580ecc | |||
c42bc74f1f | |||
35fd79f577 | |||
4ef0682e5a | |||
d0b7047189 | |||
ae3f936b66 | |||
1ef492809b | |||
5bf9dd3a9d | |||
b18a66f66b | |||
1dadfdc699 | |||
14a3f51b73 | |||
96116aeaec | |||
7fd31b6a64 | |||
299bc0db13 | |||
4b9ccae442 | |||
079ce552e3 | |||
163684e3a9 | |||
f698f561c9 | |||
cb207f18a5 | |||
76a5cdc977 | |||
a82af1d2d1 | |||
ac7d51ad80 | |||
797a739c92 | |||
666b59b463 | |||
a83d9a070c | |||
7637377471 | |||
6515f88d92 | |||
92534b9c82 | |||
c12360daa8 | |||
3d28118f3e | |||
04adfde064 | |||
d8aabfd448 | |||
cf95870d9d | |||
55f8876bcc | |||
9fb66186f0 | |||
da8fe99ca4 | |||
20374fde36 | |||
5131f722a7 | |||
1ef0a1028f | |||
0fd1f04eda | |||
5c0ba5d06c | |||
d9532b6fa0 | |||
b96065514f | |||
0e9b8e4140 | |||
d1c773360f | |||
74538bba8d | |||
64c95e3811 | |||
33fd70291a | |||
0cb4bc951a | |||
3761ab9610 | |||
8c29bba108 | |||
4e41261237 | |||
77529ef3b1 | |||
0ba96adbbc | |||
7df277c0bc | |||
c94f7b10bd | |||
83a76f7d6f | |||
6c988241eb | |||
29145dfc2d | |||
4b74e8ec70 | |||
612012aff8 | |||
6ab60d0ccd | |||
6daed83d88 | |||
f25442c036 | |||
ffffd447f2 | |||
4b511c47f0 | |||
158d471a98 | |||
107a90395c | |||
ce5fd53822 | |||
810ca78215 | |||
eb945b3315 | |||
34f24fe331 | |||
743392017d | |||
15b877f005 | |||
0653769156 | |||
3ed319872a | |||
ee124671d8 | |||
1b4dabd37c | |||
fdd253f042 | |||
1a15fdd2f0 | |||
2c66280b3f | |||
778c39d947 | |||
fa15addfb2 | |||
afbb9474b7 | |||
0acb4f8cb1 | |||
08de0938a0 | |||
2c9b80c177 | |||
e8c19df8c9 | |||
7681003512 | |||
dba8db19cb | |||
d199cdf088 | |||
f2840c5ca4 | |||
1c7a0ba4e1 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @pdcastro @thgreasi @CameronDiver @hedss
|
18
.github/ISSUE_TEMPLATE.md
vendored
18
.github/ISSUE_TEMPLATE.md
vendored
@ -1,2 +1,16 @@
|
||||
- **resin-cli version:**
|
||||
- **Operating system and architecture:**
|
||||
- **balena CLI version:** e.g. 11.2.1 (output of the `"balena version"` command)
|
||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||
- **Install method:** npm or zip or executable installer
|
||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||
|
||||
---
|
||||
|
||||
*Please keep in mind that we try to use the issue tracker of this repository for specific bug
|
||||
reports & CLI feature requests. General & troubleshooting questions are encouraged to be posted to
|
||||
the [balena forums](https://forums.balena.io), which are monitored by balena's support team and
|
||||
where the community can both contribute and benefit from the answers.*
|
||||
|
||||
*Before submitting this issue please check that this issue is not a duplicate. If there is another
|
||||
issue describing the same problem or feature please add your information to the existing issue's
|
||||
comments.*
|
||||
|
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<!-- You can remove tags that do not apply. -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
Change-type: major|minor|patch <!-- The change type of this PR -->
|
||||
|
||||
---
|
||||
##### Contributor checklist
|
||||
<!-- For completed items, change [ ] to [x]. -->
|
||||
- [ ] Introduces security considerations
|
||||
- [ ] Affects the development, build or deployment processes of the component
|
14
.gitignore
vendored
14
.gitignore
vendored
@ -24,14 +24,22 @@ build/Release
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
|
||||
node_modules
|
||||
|
||||
npm-shrinkwrap.json
|
||||
package-lock.json
|
||||
.resinconf
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
36
.resinci.yml
Normal file
36
.resinci.yml
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: darwin
|
||||
os: macos
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "8"
|
||||
- "10"
|
||||
|
||||
docker:
|
||||
publish: false
|
37
.travis.yml
37
.travis.yml
@ -1,22 +1,25 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "10"
|
||||
matrix:
|
||||
include:
|
||||
- node_js:
|
||||
- '6'
|
||||
env:
|
||||
- CAN_DEPLOY=true
|
||||
before_install:
|
||||
- npm -g install npm@4
|
||||
script: npm run ci
|
||||
exclude:
|
||||
node_js: "10"
|
||||
script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm run ci
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
notifications:
|
||||
email: false
|
||||
deploy:
|
||||
provider: npm
|
||||
email: accounts@resin.io
|
||||
api_key:
|
||||
secure: phet6Du13hc1bzStbmpwy2ODNL5BFwjAmnpJ5wMcbWfI7fl0OtQ61s2+vW5hJAvm9fiRLOfiGAEiqOOtoupShZ1X8BNkC708d8+V+iZMoFh3+j6wAEz+N1sVq471PywlOuLAscOcqQNp92giCVt+4VPx2WQYh06nLsunvysGmUM=
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$CAN_DEPLOY = 'true' && $TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: resin-io/resin-cli
|
||||
- provider: script
|
||||
script: npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: balena-io/balena-cli
|
||||
|
4528
.versionbot/CHANGELOG.yml
Normal file
4528
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
958
CHANGELOG.md
958
CHANGELOG.md
@ -4,6 +4,964 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 11.5.0 - 2019-07-05
|
||||
|
||||
* Add release target in repo.yml [Giovanni Garufi]
|
||||
* Patch oclif to use "npx npm@6.9.0 install" if npm is older than 6.9.0 [Paulo Castro]
|
||||
* Balena CI: Add balena-cli executable signing step [Paulo Castro]
|
||||
* Add 'npm run package' command [Paulo Castro]
|
||||
* Refactor build:standalone / build:installer / run release [Paulo Castro]
|
||||
* Balena CI integration: Use C:\tmp to avoid 260-char path length limit [Paulo Castro]
|
||||
|
||||
## 11.4.4 - 2019-07-02
|
||||
|
||||
* Add 'patches' to files section of package.json for npm publishing [Paulo Castro]
|
||||
* Add npm-shrinkwrap in package.json so that it gets published to the registry [Giovanni Garufi]
|
||||
|
||||
## 11.4.3 - 2019-07-01
|
||||
|
||||
* Fix "Error: Cannot find module 'web-streams-polyfill'" Fix npm-shrinkwrap.json produced by npm v6.4.1, by using npm v6.9.0 [Paulo Castro]
|
||||
|
||||
## 11.4.2 - 2019-07-01
|
||||
|
||||
* Explicitly upgrade livepush version to 2.0.1 to pick up fix [Cameron Diver]
|
||||
|
||||
## 11.4.1 - 2019-06-28
|
||||
|
||||
* Unpin selected dependencies following addition of npm-shrinkwrap.json [Paulo Castro]
|
||||
* Add npm-shrinkwrap.json file to control dependency updates [Paulo Castro]
|
||||
|
||||
## 11.4.0 - 2019-06-27
|
||||
|
||||
* Add options to 'balena version' to show Node.js version [Paulo Castro]
|
||||
* Pin the major Node version used by standalone zip packages to Node 10 [Paulo Castro]
|
||||
|
||||
## 11.3.6 - 2019-06-27
|
||||
|
||||
* Patch 'pkg' package to resolve 'preload' issue in standalone installs [Paulo Castro]
|
||||
|
||||
## 11.3.5 - 2019-06-26
|
||||
|
||||
* Add machine-readable changelog [Gergely Imreh]
|
||||
|
||||
## 11.3.4 - 2019-06-26
|
||||
|
||||
* Patterns: Add debug logs in the getOnlineTargetUuid resolution [Thodoris Greasidis]
|
||||
* Tunnel: Fix incorrect parsing of numeric short UUIDs [Thodoris Greasidis]
|
||||
* Ssh: Fix incorrect parsing of numeric short UUIDs [Thodoris Greasidis]
|
||||
|
||||
## 11.3.3 - 2019-06-20
|
||||
|
||||
* Fix using an image more than once in a balena push [Cameron Diver]
|
||||
|
||||
## 11.3.2 - 2019-06-20
|
||||
|
||||
* Remove the livepush initialisation double printed log [Cameron Diver]
|
||||
|
||||
## 11.3.1 - 2019-06-18
|
||||
|
||||
* Fix output of seperation newline during livepush [Cameron Diver]
|
||||
|
||||
## 11.3.0 - 2019-06-18
|
||||
|
||||
* If a secrets file is not specified, read it from the data directory [Cameron Diver]
|
||||
|
||||
## 11.2.2 - 2019-06-16
|
||||
|
||||
* Docs: update GitHub issue template, required Node version and sample Dockerfile [Paulo Castro]
|
||||
|
||||
## 11.2.1 - 2019-06-12
|
||||
|
||||
* Livepush: Ignore the .git directory when performing a livepush [Cameron Diver]
|
||||
|
||||
## 11.2.0 - 2019-06-11
|
||||
|
||||
* Add device OS update action [Thodoris Greasidis]
|
||||
|
||||
## 11.1.0 - 2019-06-10
|
||||
|
||||
* Cancel ongoing livepushes when a new change occurs [Cameron Diver]
|
||||
* Fix ts-node invocation in balena-dev [Cameron Diver]
|
||||
|
||||
## 11.0.7 - 2019-06-07
|
||||
|
||||
* Fix "catch-uncommitted" build failure (npm run prettify) [Paulo Castro]
|
||||
* Update tunnel documentation after argument changes [Cameron Diver]
|
||||
|
||||
## 11.0.6 - 2019-06-06
|
||||
|
||||
* Fix: Add single code path to get full, online-only device UUIDs [Rich Bayliss]
|
||||
|
||||
## 11.0.5 - 2019-06-06
|
||||
|
||||
* Add initial typings for resin-cli-form [Thodoris Greasidis]
|
||||
|
||||
## 11.0.4 - 2019-06-05
|
||||
|
||||
* Add 'scan' command to the website reference documentation [Paulo Castro]
|
||||
|
||||
## 11.0.3 - 2019-06-05
|
||||
|
||||
* Fix 'npm help' SyntaxError on Node 8 (invalid 's' regex flag) [Paulo Castro]
|
||||
|
||||
## 11.0.2 - 2019-06-05
|
||||
|
||||
* Fix "--production" installation (missing patch-package dependency) [Paulo Castro]
|
||||
|
||||
## 11.0.1 - 2019-06-04
|
||||
|
||||
* Fix Travis release [Paulo Castro]
|
||||
|
||||
## 11.0.0 - 2019-06-04
|
||||
|
||||
* Remove 'signup' command [Paulo Castro]
|
||||
* Revert bin/balena (previously renamed bin/run for oclif compatibility) [Paulo Castro]
|
||||
* Balena CI integration: Patch @oclif/dev-cli to install 7zip on demand [Paulo Castro]
|
||||
* Add warning notices for replaced 'local' commands in v11 [Paulo Castro]
|
||||
* Revert 'balena flash' to 'balena local flash' [Paulo Castro]
|
||||
* Fix SSH'ing into a device from application [Cameron Diver]
|
||||
* Make livepush the default when pushing to a local device [Cameron Diver]
|
||||
* Make the CommandDefinition option parameter a Partial [Cameron Diver]
|
||||
* Allow multiple services to be tailed with balena logs and push [Cameron Diver]
|
||||
* Don't require a login for commands operating on local devices [Cameron Diver]
|
||||
* Remove or move most local namespaced commands [Cameron Diver]
|
||||
* Update the CLI's installation instructions for executable installers [Paulo Castro]
|
||||
* Add native installers for Windows and macOS [Paulo Castro]
|
||||
* Bump denymount version and delete redundant patch (chore task) [Paulo Castro]
|
||||
* Add CONTRIBUTING.md and some guidance on commit messages and doc files. [Paulo Castro]
|
||||
* Update documentation markdown following v11-meta branch rebase [Paulo Castro]
|
||||
* Fix windows straight-to-container SSH [Cameron Diver]
|
||||
* Update balena ssh command to support local devices and multicontainer [Cameron Diver]
|
||||
* Sort 'balena help' primary commands in manually specified order [Paulo Castro]
|
||||
* Remove --dockerPort's -p alias for `balena preload` [Alexis Svinartchouk]
|
||||
* Begin the transition to oclif with 'balena env add' (fix dropped leading zero in device UUID). [Paulo Castro]
|
||||
* Bump min Node.js version to 8.0, ts-node to 8.1 and typescript to 3.4. Refactor typings folder for use with the tsconfig typeRoots option. [Paulo Castro]
|
||||
|
||||
## 10.17.5 - 2019-06-04
|
||||
|
||||
* Pin moment-duration-format package (ReferenceError: window is not defined) [Paulo Castro]
|
||||
|
||||
## 10.17.4 - 2019-06-03
|
||||
|
||||
* .gitignore: Add fast-boot.json generated by balena-dev command [Thodoris Greasidis]
|
||||
|
||||
## 10.17.3 - 2019-05-31
|
||||
|
||||
* Use an .npmrc to prevent creating a package-lock on each install [Thodoris Greasidis]
|
||||
|
||||
## 10.17.2 - 2019-05-30
|
||||
|
||||
* Allow newline characters in build/deploy --buildArg values [Paulo Castro]
|
||||
|
||||
## 10.17.1 - 2019-05-30
|
||||
|
||||
* Fix CI build error (missing @types/mz) [Paulo Castro]
|
||||
|
||||
## 10.17.0 - 2019-05-29
|
||||
|
||||
* Add preload --add-certificate option [Alexis Svinartchouk]
|
||||
|
||||
## 10.16.0 - 2019-05-27
|
||||
|
||||
* Compose: remove artificial 20 repo limit [Matthew McGinn]
|
||||
|
||||
## 10.15.0 - 2019-05-27
|
||||
|
||||
* Add a much faster container replacement for livepush [Cameron Diver]
|
||||
* Add a containerId request function to the device api module [Cameron Diver]
|
||||
|
||||
## 10.14.0 - 2019-05-27
|
||||
|
||||
* Add the ability to specify an environment variable when pushing to local mode device [Cameron Diver]
|
||||
|
||||
## 10.13.6 - 2019-05-22
|
||||
|
||||
* Improve preload's --commit parameter description [Alexis Svinartchouk]
|
||||
* Fix `balena preload --commit current` alias [Alexis Svinartchouk]
|
||||
|
||||
## 10.13.5 - 2019-05-21
|
||||
|
||||
* Rename `preload --commit latest` to `preload --commit current` [Alexis Svinartchouk]
|
||||
* Update balena-preload to 8.1.4 [Alexis Svinartchouk]
|
||||
|
||||
## 10.13.4 - 2019-05-20
|
||||
|
||||
* Fix TypeError when running 'balena apps' [Paulo Castro]
|
||||
|
||||
## 10.13.3 - 2019-05-17
|
||||
|
||||
* Apps: Fix the device count columns being empty [Thodoris Greasidis]
|
||||
|
||||
## 10.13.2 - 2019-05-17
|
||||
|
||||
* Remove intermediate containers when doing a local push [Cameron Diver]
|
||||
|
||||
## 10.13.1 - 2019-05-16
|
||||
|
||||
* Docs: Fix os configure example in os build-config docs [Thodoris Greasidis]
|
||||
|
||||
## 10.13.0 - 2019-05-15
|
||||
|
||||
* Use the open-balena-api endpoints for device type & version info [Thodoris Greasidis]
|
||||
|
||||
## 10.12.1 - 2019-05-15
|
||||
|
||||
* Preload: bump version to fix preloading on logstream supervisors [Gergely Imreh]
|
||||
|
||||
## 10.12.0 - 2019-05-15
|
||||
|
||||
* Fix video url [Daniel Andrade]
|
||||
|
||||
## 10.11.1 - 2019-05-15
|
||||
|
||||
* Debounce livepush invocations to collect changes together [Cameron Diver]
|
||||
|
||||
## 10.11.0 - 2019-05-15
|
||||
|
||||
* Update balena-cli video url [Daniel Andrade]
|
||||
|
||||
## 10.10.5 - 2019-05-14
|
||||
|
||||
* Update instructions for adding folder to path in MacOS [Jasmine Gilbert]
|
||||
|
||||
## 10.10.4 - 2019-05-14
|
||||
|
||||
* Pin pkg version to avoid node 6 error [Cameron Diver]
|
||||
* Use TCP keepalive probes to detect local log stream closing [Cameron Diver]
|
||||
|
||||
## 10.10.3 - 2019-05-10
|
||||
|
||||
* Fix 'local configure' on macOS standalone installation [Paulo Castro]
|
||||
|
||||
## 10.10.2 - 2019-05-10
|
||||
|
||||
* Update dependencies including a balena-preload fix for lots of releases [Pagan Gazzard]
|
||||
|
||||
## 10.10.1 - 2019-05-04
|
||||
|
||||
* Replace 'npm' upgrade notifier message with INSTALL.md URL [Paulo Castro]
|
||||
|
||||
## 10.10.0 - 2019-05-03
|
||||
|
||||
* Qemu: use v4.0.0-balena [Gergely Imreh]
|
||||
|
||||
## 10.9.4 - 2019-05-02
|
||||
|
||||
* Improve livepush UX [Cameron Diver]
|
||||
* Improve logging for detached mode + livepush [Cameron Diver]
|
||||
|
||||
## 10.9.3 - 2019-05-02
|
||||
|
||||
* Actions/auth: fix mixed indentation error [Gergely Imreh]
|
||||
* Dependencies: bump gulp to v4 [Gergely Imreh]
|
||||
* Dependencies: bump etcher-sdk to pull in fixes [Gergely Imreh]
|
||||
|
||||
## 10.9.2 - 2019-05-01
|
||||
|
||||
* Update README and INSTALL docs (review typos and some rewording) [Paulo Castro]
|
||||
|
||||
## 10.9.1 - 2019-05-01
|
||||
|
||||
* Allow any amount of subdomains when parsing .local addresses [Cameron Diver]
|
||||
|
||||
## 10.9.0 - 2019-04-29
|
||||
|
||||
* Unify the CLI instructions between capitanodoc.ts and README.md, move the installation instructions to INSTALL.md, and update the markdown generation scripts. [Paulo Castro]
|
||||
|
||||
## 10.8.2 - 2019-04-26
|
||||
|
||||
* Handle app names that look like a number (eg 1234) [Paulo Castro]
|
||||
|
||||
## 10.8.1 - 2019-04-26
|
||||
|
||||
* Add better semantics for detached mode + live for push [Cameron Diver]
|
||||
|
||||
## 10.8.0 - 2019-04-25
|
||||
|
||||
* Allow specifying a .local address for logs and push [Cameron Diver]
|
||||
|
||||
## 10.7.0 - 2019-04-24
|
||||
|
||||
* Allow filtering of system logs with push and logs commands [Cameron Diver]
|
||||
|
||||
## 10.6.0 - 2019-04-24
|
||||
|
||||
* Add per-service filtering to logs and push [Cameron Diver]
|
||||
|
||||
## 10.5.0 - 2019-04-24
|
||||
|
||||
* Push: Add detached flag to avoid streaming logs after local push [Cameron Diver]
|
||||
|
||||
## 10.4.1 - 2019-04-24
|
||||
|
||||
* Add ability to use balena logs with a local mode device [Cameron Diver]
|
||||
* Refactor: Convert logs action to typescript [Cameron Diver]
|
||||
* Logs: Make device logs consistent across the CLI [Cameron Diver]
|
||||
* Refactor: Create and use validation functions for input [Cameron Diver]
|
||||
|
||||
## 10.4.0 - 2019-04-24
|
||||
|
||||
* Tunnel: allow using partial device uuids [Will Boyce]
|
||||
|
||||
## 10.3.0 - 2019-04-23
|
||||
|
||||
* Add --dockerfile option to the build, deploy and push commands [Paulo Castro]
|
||||
* Fix push and deploy issues under Windows ('/' vs '\' path separators) [Paulo Castro]
|
||||
|
||||
## 10.2.0 - 2019-04-23
|
||||
|
||||
* Update livepush documentation and required versions [Cameron Diver]
|
||||
* Cleanup intermediate containers on exit of livepush [Cameron Diver]
|
||||
* Livepush: Perform full rebuild on Dockerfile-like file change [Cameron Diver]
|
||||
* Add livepush ability to balena push [Cameron Diver]
|
||||
* Log: Add livepush logging functions [Cameron Diver]
|
||||
* Add device status endpoint api function [Cameron Diver]
|
||||
|
||||
## 10.1.1 - 2019-04-22
|
||||
|
||||
* Ensure not marking successful releases as canceled [Thodoris Greasidis]
|
||||
|
||||
## 10.1.0 - 2019-04-18
|
||||
|
||||
* Updated CLI installation notes on README.md and ran prettier [Paulo Castro]
|
||||
* Add more information about the stantalone version [Daniel Andrade]
|
||||
|
||||
## 10.0.1 - 2019-04-13
|
||||
|
||||
* Fix docs markdown (deprecation messages for 'local push' and 'sync') [Paulo Castro]
|
||||
* Fix file ignore rules matching metadata folders breaking qemu builds [Akis Kesoglou]
|
||||
* Remove information about livepush in sync deprecation message [Cameron Diver]
|
||||
|
||||
## 10.0.0 - 2019-04-03
|
||||
|
||||
* Remove 'quickstart' command and deprecate 'local push'. [Paulo Castro]
|
||||
|
||||
## 9.15.6 - 2019-03-28
|
||||
|
||||
* Support nocache flag in push <ip> [Cameron Diver]
|
||||
|
||||
## 9.15.5 - 2019-03-27
|
||||
|
||||
* Bump docker-progress (4.0.0) to improve `balena deploy` error handling. [Paulo Castro]
|
||||
|
||||
## 9.15.4 - 2019-03-25
|
||||
|
||||
* Update resin-compose-parse to v2.0.4 [Pablo Carranza Velez]
|
||||
* Update resin-multibuild to v2.1.5 [Pablo Carranza Velez]
|
||||
|
||||
## 9.15.3 - 2019-03-25
|
||||
|
||||
* Allow 'balena push <deviceIpAddress>' when not logged in to balenaCloud. [Paulo Castro]
|
||||
|
||||
## 9.15.2 - 2019-03-18
|
||||
|
||||
* Bump resin-multibuild (2.1.4), docker-progress (3.0.5), resin-lint (3.0.1) [Paulo Castro]
|
||||
|
||||
## 9.15.1 - 2019-03-12
|
||||
|
||||
* Add maintainer, reviewers, and devexp team as code owners [Gergely Imreh]
|
||||
|
||||
## 9.15.0 - 2019-03-12
|
||||
|
||||
* Ssh: add `--noninteractive` flag not to suggest devices to connect to [Gergely Imreh]
|
||||
|
||||
## 9.14.7 - 2019-03-11
|
||||
|
||||
* Ssh: correct the minimum OS version that allows host OS connection [Gergely Imreh]
|
||||
|
||||
## 9.14.6 - 2019-03-07
|
||||
|
||||
* Fix 'unauthorized' error for additional members of legacy apps [Paulo Castro]
|
||||
|
||||
## 9.14.5 - 2019-03-05
|
||||
|
||||
* Retry image push a few times (balena deploy, 'unknown blob') [Paulo Castro]
|
||||
* Harden 'remote-build' error handling (balena push) [Paulo Castro]
|
||||
|
||||
## 9.14.4 - 2019-03-05
|
||||
|
||||
* Update resin-multibuild to pick up fixes [Cameron Diver]
|
||||
|
||||
## 9.14.3 - 2019-03-04
|
||||
|
||||
* Minor doc updates (add DEBUG hint) [Paulo Castro]
|
||||
|
||||
## 9.14.2 - 2019-03-01
|
||||
|
||||
* Bump resin-multibuild version to fix docker-compose 'context' issue [Paulo Castro]
|
||||
|
||||
## 9.14.1 - 2019-02-27
|
||||
|
||||
* Add registry-secrets help msg for build and deploy commands [Paulo Castro]
|
||||
|
||||
## 9.14.0 - 2019-02-27
|
||||
|
||||
* Extend private registry support to balena build and deploy commands [Paulo Castro]
|
||||
|
||||
## 9.13.0 - 2019-02-27
|
||||
|
||||
* Integrate new resin-multibuild major version (private docker registry authentication support for the docker-compose 'image' instruction). [Paulo Castro]
|
||||
|
||||
## 9.12.7 - 2019-02-26
|
||||
|
||||
* Fix parsing of not-really-numeric device UUID parameters [Paulo Castro]
|
||||
|
||||
## 9.12.6 - 2019-02-22
|
||||
|
||||
* Fix regression in ee75ff and restore functionality to ToC in docs [Chris Crocker-White]
|
||||
|
||||
## 9.12.5 - 2019-02-22
|
||||
|
||||
* Re-run newest prettier on master [Thodoris Greasidis]
|
||||
|
||||
## 9.12.4 - 2019-02-20
|
||||
|
||||
* Tunnel: Refactor to improve log output [Rich Bayliss]
|
||||
|
||||
## 9.12.3 - 2019-02-15
|
||||
|
||||
* Tunnel: Add the tunnel command [Rich Bayliss]
|
||||
|
||||
## 9.12.2 - 2019-02-07
|
||||
|
||||
* Remove fetching of Mixpanel token [Paulo Castro]
|
||||
|
||||
## 9.12.1 - 2019-02-05
|
||||
|
||||
* Rename localcommit to localrelease in target state for local mode [Cameron Diver]
|
||||
|
||||
## 9.12.0 - 2019-01-22
|
||||
|
||||
* Don't offer to disable automatic application updates when using pinning [Alexis Svinartchouk]
|
||||
|
||||
## 9.11.2 - 2019-01-17
|
||||
|
||||
* Update 'balena help local flash' documentation re zipped images [Paulo Castro]
|
||||
|
||||
## 9.11.1 - 2019-01-16
|
||||
|
||||
* Typescript: Add TypeScript migration notice to README file [Paulo Castro]
|
||||
|
||||
## 9.11.0 - 2019-01-16
|
||||
|
||||
* Support compressed images in `balena local flash` [Alexis Svinartchouk]
|
||||
|
||||
## 9.10.1 - 2019-01-15
|
||||
|
||||
* Fix up small docs typo [Matthew McGinn]
|
||||
|
||||
## 9.10.0 - 2019-01-14
|
||||
|
||||
* Improve startup time by adding fast-boot [Shaun Mulligan]
|
||||
|
||||
## 9.9.4 - 2019-01-13
|
||||
|
||||
* Lazy load the sdk as much as possible [Pagan Gazzard]
|
||||
|
||||
## 9.9.3 - 2019-01-13
|
||||
|
||||
* Lazy-load docker-toolbelt [Pagan Gazzard]
|
||||
|
||||
## 9.9.2 - 2019-01-11
|
||||
|
||||
* Lazy-load etcher-sdk to speed up startup [Pagan Gazzard]
|
||||
* Lazy-load resin-cli-form and resin-cli-visuals to speed up startup [Pagan Gazzard]
|
||||
|
||||
## 9.9.1 - 2019-01-11
|
||||
|
||||
* Update util available-drives action [Alexis Svinartchouk]
|
||||
* Update lib/actions/local/flash.coffee [Alexis Svinartchouk]
|
||||
|
||||
## 9.9.0 - 2019-01-10
|
||||
|
||||
* Request access to previously pushed release via `balena deploy` [Matthew McGinn]
|
||||
|
||||
## 9.8.0 - 2019-01-01
|
||||
|
||||
* Escape backticks in JS template literal [Trevor Sullivan]
|
||||
* Moving docs from PR #1055 [Trevor Sullivan]
|
||||
|
||||
## 9.7.0 - 2018-12-28
|
||||
|
||||
* Added documentation about the dependencies required to build balena-cli [Trevor Sullivan]
|
||||
|
||||
## 9.6.0 - 2018-12-18
|
||||
|
||||
* Add push --registry-secrets option for private docker registry authentication [Paulo Castro]
|
||||
|
||||
## 9.5.0 - 2018-12-14
|
||||
|
||||
* Add explicit device type option to `os configure` & `config generate` [Thodoris Greasidis]
|
||||
|
||||
## 9.4.1 - 2018-12-14
|
||||
|
||||
* Fix deploy action on node 6 [Alexis Svinartchouk]
|
||||
|
||||
## 9.4.0 - 2018-12-10
|
||||
|
||||
* Actions: Add resource tag operations [Thodoris Greasidis]
|
||||
|
||||
## 9.3.6 - 2018-12-03
|
||||
|
||||
* Make architecture checking more stringent when installing emulators [Cameron Diver]
|
||||
|
||||
## 9.3.5 - 2018-11-28
|
||||
|
||||
* Fix potential dependency security issue [Thodoris Greasidis]
|
||||
|
||||
## 9.3.4 - 2018-11-28
|
||||
|
||||
* Update dependencies [Cameron Diver]
|
||||
|
||||
## 9.3.3 - 2018-11-27
|
||||
|
||||
* Actions/os-initialize: Convert Promise to a Bluebird one [Thodoris Greasidis]
|
||||
|
||||
## 9.3.2 - 2018-11-26
|
||||
|
||||
* Actions/os: Fix os configure using bluebird methods on plain promise [Thodoris Greasidis]
|
||||
|
||||
## 9.3.1 - 2018-11-23
|
||||
|
||||
* Actions/config: Fix examples to include --version as required [Thodoris Greasidis]
|
||||
|
||||
## 9.3.0 - 2018-11-22
|
||||
|
||||
* Stop pinning ext2fs and update preload to ^8.0.4 [Alexis Svinartchouk]
|
||||
|
||||
## 9.2.2 - 2018-11-20
|
||||
|
||||
* Fix missing import in `os configure` [Akis Kesoglou]
|
||||
|
||||
## 9.2.1 - 2018-11-19
|
||||
|
||||
* Add case-insensitive checking for application names in balena push [Cameron Diver]
|
||||
|
||||
## 9.2.0 - 2018-11-16
|
||||
|
||||
* Make specifying the version during configuration optional [Akis Kesoglou]
|
||||
|
||||
## 9.1.4 - 2018-11-16
|
||||
|
||||
* Extend the github repository templates [Thodoris Greasidis]
|
||||
|
||||
## 9.1.3 - 2018-11-16
|
||||
|
||||
* Include `join` and `leave` commands in API documentation [Akis Kesoglou]
|
||||
|
||||
## 9.1.2 - 2018-11-13
|
||||
|
||||
* Fix build & deploy commands in standalone build [Tim Perry]
|
||||
|
||||
## 9.1.1 - 2018-11-12
|
||||
|
||||
* Fix the architecture string used when downloading qemu versions [Cameron Diver]
|
||||
|
||||
## 9.1.0 - 2018-11-07
|
||||
|
||||
* Update qemu to v3, and automatically use the correct architecture (arm/aarch64) [Edward Keeble]
|
||||
|
||||
## 9.0.3 - 2018-11-07
|
||||
|
||||
* Mark api keys in the CLI as non-experimental [Tim Perry]
|
||||
|
||||
## 9.0.2 - 2018-11-06
|
||||
|
||||
* Stop validating device config, now that it's API-generated [Tim Perry]
|
||||
|
||||
## v9.0.1 - 2018-11-01
|
||||
|
||||
* Dont ignore balena metadata directories when balena pushing [Cameron Diver]
|
||||
|
||||
## v9.0.0 - 2018-10-29
|
||||
|
||||
* Change env var commands to set app-wide env vars, using the new SDK [Tim Perry]
|
||||
* Utils/promote: Use the application slug for filtering & presenting [Thodoris Greasidis]
|
||||
* Rename everything from 'resin' to 'balena' [Tim Perry]
|
||||
* Sort device types by name [Thodoris Greasidis]
|
||||
* Utils/promote: Do not rely on the user to always be there [Thodoris Greasidis]
|
||||
* Add support for the Opensource provisioning flow [Thodoris Greasidis]
|
||||
* Add rename notice to resin-cli [Paulo Castro]
|
||||
|
||||
## v8.1.0 - 2018-10-24
|
||||
|
||||
* README: Add development guidelines section [Thodoris Greasidis]
|
||||
* Chore: Add on the fly transpiled bin [Thodoris Greasidis]
|
||||
|
||||
## v8.0.3 - 2018-10-22
|
||||
|
||||
* Ssh: Move from resin-sdk-preconfigured to resin-sdk [Will Boyce]
|
||||
|
||||
## v8.0.2 - 2018-10-20
|
||||
|
||||
* Utils/patterns: Drop resin-sdk-preconfigured [Thodoris Greasidis]
|
||||
* Actions/app: Drop resin-sdk-preconfigured [Thodoris Greasidis]
|
||||
|
||||
## v8.0.1 - 2018-10-20
|
||||
|
||||
* Update dockerignore to fix escSL bug [Tim Perry]
|
||||
|
||||
## v8.0.0 - 2018-10-19
|
||||
|
||||
* Support multicontainer local mode in resin push [Cameron Diver]
|
||||
* Stop accepting resin-compose.yml as a build composition definition [Cameron Diver]
|
||||
* Send push source packages as gzipped data [Cameron Diver]
|
||||
* Respect ignore files when tarring sources [Cameron Diver]
|
||||
* Check for correct architecture when preloading, instead of correct device type [Alexis Svinartchouk]
|
||||
* Default preload boolean parameters to false [Alexis Svinartchouk]
|
||||
* Correctly error out on failed remote builds [Cameron Diver]
|
||||
* Bump tsconfig target to es6 [Cameron Diver]
|
||||
|
||||
## v7.10.9 - 2018-10-18
|
||||
|
||||
* Update sdk references in wizzard.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.8 - 2018-10-18
|
||||
|
||||
* Update sdk references in notes.coffee [Scott Lowe]
|
||||
* Update sdk references in device.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.7 - 2018-10-18
|
||||
|
||||
* Update sdk sdk references in auth.coffee [Scott Lowe]
|
||||
|
||||
## v7.10.6 - 2018-10-03
|
||||
|
||||
* Fix formatting of preload examples [Tim Perry]
|
||||
|
||||
## v7.10.5 - 2018-09-25
|
||||
|
||||
* README: Fix typo [Lucian Buzzo]
|
||||
|
||||
## v7.10.4 - 2018-09-24
|
||||
|
||||
* Device: When registering, print the uuid [Pablo Carranza Velez]
|
||||
|
||||
## v7.10.3 - 2018-09-19
|
||||
|
||||
* Include --emulated in the example resin build parameters [Tim Perry]
|
||||
|
||||
## v7.10.2 - 2018-09-18
|
||||
|
||||
* Dependencies: Update resin-semver version to support Balena OS [Lucian Buzzo]
|
||||
|
||||
## v7.10.1 - 2018-09-11
|
||||
|
||||
* Stop Travis deploying to npm (now handled by concourse) [Tim Perry]
|
||||
|
||||
## v7.10.0 - 2018-09-11
|
||||
|
||||
* Update resin-cli-form to 2.x [Pagan Gazzard]
|
||||
|
||||
## v7.9.4 - 2018-09-10
|
||||
|
||||
* Device api keys are no longer used in the registration process [Theodor Gherzan]
|
||||
|
||||
## v7.9.3 - 2018-08-20
|
||||
|
||||
* Fix configuration hangs with some images by expanding the threadpool #952 [Tim Perry]
|
||||
|
||||
## v7.9.2 - 2018-08-15
|
||||
|
||||
* Add warning about re-enabling automatic updates #946 [Pagan Gazzard]
|
||||
|
||||
## v7.9.1 - 2018-08-15
|
||||
|
||||
* Fix errors in `getRequestStream` not being propogated #942 [Pagan Gazzard]
|
||||
|
||||
## v7.9.0 - 2018-08-09
|
||||
|
||||
* Support emulated and nocache options for remote builds #903 [Cameron Diver]
|
||||
|
||||
## v7.8.6 - 2018-08-09
|
||||
|
||||
* Fix bug where the sudo helper failed in os initialize #939 [Tim Perry]
|
||||
|
||||
## v7.8.5 - 2018-08-09
|
||||
|
||||
* Update .resin-sync.yml docs for local push and include example env vars #934 [Tim Perry]
|
||||
|
||||
## v7.8.4 - 2018-08-02
|
||||
|
||||
* Update klaw #936 [Tim Perry]
|
||||
|
||||
## v7.8.3 - 2018-07-25
|
||||
|
||||
* Follow links found during builds #931 [Tim Perry]
|
||||
|
||||
## v7.8.2 - 2018-07-25
|
||||
|
||||
* Update reconfix to fix volume signature errors in local configure #929 [Tim Perry]
|
||||
|
||||
## v7.8.1 - 2018-07-20
|
||||
|
||||
* Be explicit about how much initial history log tailing includes #930 [Tim Perry]
|
||||
|
||||
## v7.8.0 - 2018-07-20
|
||||
|
||||
* Add join/leave commands to promote and move devices between platforms #895 [Akis Kesoglou]
|
||||
|
||||
## v7.7.4 - 2018-07-17
|
||||
|
||||
* Update TypeScript to 2.8.1 #923 [Tim Perry]
|
||||
* Add --version options to os configure & config generate #923 [Tim Perry]
|
||||
* Update OS & config actions to the latest SDK #923 [Tim Perry]
|
||||
|
||||
## v7.7.3 - 2018-07-13
|
||||
|
||||
* Update the deploy key since npm invalidated the old one #927 [Tim Perry]
|
||||
|
||||
## v7.7.2 - 2018-07-13
|
||||
|
||||
* Pin ext2fs to 1.0.7 to avoid temporary deployment issues #926 [Tim Perry]
|
||||
|
||||
## v7.7.1 - 2018-07-12
|
||||
|
||||
* Update CLI to SDK v10 (include new API logs) #925 [Tim Perry]
|
||||
|
||||
## v7.7.0 - 2018-07-11
|
||||
|
||||
* Add --generate-device-api-key parameter to config generate #921 [Tim Perry]
|
||||
|
||||
## v7.6.2 - 2018-06-28
|
||||
|
||||
* Make local commands more resilient to unnamed containers #910 [Tim Perry]
|
||||
|
||||
## v7.6.1 - 2018-06-26
|
||||
|
||||
* Make sure 'resin push' is included in the docs #907 [Tim Perry]
|
||||
|
||||
## v7.6.0 - 2018-06-20
|
||||
|
||||
* Support pinned release preloading #896 [Cameron Diver]
|
||||
|
||||
## v7.5.2 - 2018-06-12
|
||||
|
||||
* Document Pyhton native build dependency #893 [Tim Perry]
|
||||
|
||||
## v7.5.1 - 2018-06-01
|
||||
|
||||
* Add a multicontainer caveat to the env var commands #887 [Tim Perry]
|
||||
|
||||
## v7.5.0 - 2018-05-31
|
||||
|
||||
* Update resin-compose-parse dependency version to 1.10.2 #883 [Ariel Flesler]
|
||||
|
||||
## v7.4.1 - 2018-05-24
|
||||
|
||||
* Update SDK in resin device(s) to ensure the dashboard URL is correct #879 [Tim Perry]
|
||||
|
||||
## v7.4.0 - 2018-05-10
|
||||
|
||||
* Add push command which starts a build on remote resin servers #868 [Cameron Diver]
|
||||
|
||||
## v7.3.8 - 2018-05-03
|
||||
|
||||
* Catch require errors and provide helpful instructions #874 [Tim Perry]
|
||||
* Inline the entire resin-cli-errors module #874 [Tim Perry]
|
||||
|
||||
## v7.3.7 - 2018-04-30
|
||||
|
||||
* Pin node types to v9.0.0 to avoid build errors with transient dependencies #871 [Cameron Diver]
|
||||
|
||||
## v7.3.6 - 2018-04-30
|
||||
|
||||
* Update resin-image-fs to stop non-config commands failing in node 10 #870 [Tim Perry]
|
||||
|
||||
## v7.3.5 - 2018-04-18
|
||||
|
||||
* Don't show Docker container status from devices, as it can be wrong #858 [Tim Perry]
|
||||
|
||||
## v7.3.4 - 2018-04-18
|
||||
|
||||
* Include resin compose schemas in the standalone build #865 [Tim Perry]
|
||||
|
||||
## v7.3.3 - 2018-04-17
|
||||
|
||||
* Don't report lots of user input errors #861 [Tim Perry]
|
||||
* Include Sentry breadcrumbs for context in error reports #861 [Tim Perry]
|
||||
* Update to Sentry 2.x #861 [Tim Perry]
|
||||
|
||||
## v7.3.2 - 2018-04-16
|
||||
|
||||
* Update Dockerode to fix local push issue in standalone builds #862 [Tim Perry]
|
||||
|
||||
## v7.3.1 - 2018-04-13
|
||||
|
||||
* Update resin-compose-parse to v1.8.1 to fix a problem parsing ports #849 [Pablo Carranza Velez]
|
||||
|
||||
## v7.3.0 - 2018-04-12
|
||||
|
||||
* Add 'api-key generate' command #854 [Tim Perry]
|
||||
|
||||
## v7.2.4 - 2018-04-10
|
||||
|
||||
* Explicitly depend on tar-stream #852 [Tim Perry]
|
||||
* Correct documented node version requirement to 6+ #852 [Tim Perry]
|
||||
|
||||
## v7.2.3 - 2018-04-06
|
||||
|
||||
* Add a fast build script to package.json #847 [Cameron Diver]
|
||||
|
||||
## v7.2.2 - 2018-04-04
|
||||
|
||||
* Throw a clear error when logging in with an invalid token #846 [Tim Perry]
|
||||
|
||||
## v7.2.1 - 2018-03-29
|
||||
|
||||
* Update docker-qemu-transpose to avoid the broken 0.4.1 release #839 [Tim Perry]
|
||||
|
||||
## v7.2.0 - 2018-03-29
|
||||
|
||||
* Do not require a login for builds #835 [Tim Perry]
|
||||
* Allow (experimental!) login with API keys #835 [Tim Perry]
|
||||
|
||||
## v7.1.6 - 2018-03-29
|
||||
|
||||
* Fix build emulation for multi-stage builds #838 [Tim Perry]
|
||||
|
||||
## v7.1.5 - 2018-03-27
|
||||
|
||||
* Fix crash when an app is not specified for build command #834 [Akis Kesoglou]
|
||||
|
||||
## v7.1.4 - 2018-03-26
|
||||
|
||||
* Upgrade resin-sync to pull in the fix for #824 #832 [Tim Perry]
|
||||
|
||||
## v7.1.3 - 2018-03-23
|
||||
|
||||
* Prefix all pine options with '$' in preload to avoid pine warnings. #831 [Alexis Svinartchouk]
|
||||
|
||||
## v7.1.2 - 2018-03-23
|
||||
|
||||
* Update resin-preload to 6.2.0 and resin-sdk to 9.0.0-beta16 #830 [Alexis Svinartchouk]
|
||||
|
||||
## v7.1.1 - 2018-03-22
|
||||
|
||||
* Remove explicit anchor links in CLI docs #827 [Zach Walchuk]
|
||||
|
||||
## v7.1.0 - 2018-03-22
|
||||
|
||||
* Warn early if deploying a multicontainer project to an incompatible app #818 [Akis Kesoglou]
|
||||
* Add legacy deploy method back #818 [Akis Kesoglou]
|
||||
|
||||
## v7.0.7 - 2018-03-20
|
||||
|
||||
* Update resin-preload to 6.1.2 #821 [Alexis Svinartchouk]
|
||||
|
||||
## v7.0.6 - 2018-03-20
|
||||
|
||||
* Make sure image name is all lowercase #815 [Akis Kesoglou]
|
||||
* Improve handling of build log output #815 [Akis Kesoglou]
|
||||
|
||||
## v7.0.5 - 2018-03-15
|
||||
|
||||
* Add bash completions #801 [Ronald McCollam]
|
||||
|
||||
## v7.0.4 - 2018-03-15
|
||||
|
||||
* Generate consistent working anchors for both our md output & resin docs #813 [Tim Perry]
|
||||
|
||||
## v7.0.3 - 2018-03-15
|
||||
|
||||
* Fix getting window size when there’s no TTY attached #812 [Akis Kesoglou]
|
||||
|
||||
## v7.0.2 - 2018-03-13
|
||||
|
||||
* Update full CLI docs with recent installation improvements too #807 [Tim Perry]
|
||||
|
||||
## v7.0.1 - 2018-03-12
|
||||
|
||||
* Recommend using unsafe-prem to avoid permission issues on install #805 [Tim Perry]
|
||||
* Remove unnecessary resin-cli-auth dependency #805 [Tim Perry]
|
||||
|
||||
## v7.0.0 - 2018-03-09
|
||||
|
||||
* Add docker-compose-aware builds and deployments #792 [Akis Kesoglou]
|
||||
* *BREAKING*: Remove support for plugins entirely #792 [Tim Perry]
|
||||
* Update dashboard login to use the multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
* Multicontainer preload: Update resin-preload to 6.0.0-beta4 #792 [Alexis Svinartchouk]
|
||||
* Update the keys action to use the multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
* Require multicontainer SDK #792 [Alexis Svinartchouk]
|
||||
|
||||
## v6.13.5 - 2018-03-07
|
||||
|
||||
* Fix prettier configuration to avoid linting errors #802 [Tim Perry]
|
||||
|
||||
## v6.13.4 - 2018-02-22
|
||||
|
||||
* Fix issue where emulated builds broke Docker `ENV` commands #796 [Gergely Imreh]
|
||||
|
||||
## v6.13.3 - 2018-02-20
|
||||
|
||||
* Tweak TS & add missing deps that may cause build failures in some envs #793 [Tim Perry]
|
||||
|
||||
## v6.13.2 - 2018-02-20
|
||||
|
||||
* Ensure login does not wait for the browser process to close #794 [Tim Perry]
|
||||
|
||||
## v6.13.1 - 2018-02-07
|
||||
|
||||
* Move to the correct coffeescript (no hyphen) dependency #786 [Tim Perry]
|
||||
* Add typings for 'ent' #786 [Tim Perry]
|
||||
|
||||
## v6.13.0 - 2018-02-06
|
||||
|
||||
* Add support for Balena in local ssh #777 [Tim Perry]
|
||||
|
||||
## v6.12.9 - 2018-02-05
|
||||
|
||||
* Switch back to upstream global-tunnel-ng #781 [Alexis Svinartchouk]
|
||||
|
||||
## v6.12.8 - 2018-02-03
|
||||
|
||||
* Fix uuid params being parsed a numbers #774 [Thodoris Greasidis]
|
||||
|
||||
## v6.12.7 - 2018-01-30
|
||||
|
||||
* Add 'or mounted resinOS image' #767 [MoranF]
|
||||
|
||||
## v6.12.6 - 2018-01-29
|
||||
|
||||
* Don't use the deprecated 'os configure' format in internal calls #759 [Tim Perry]
|
||||
|
||||
## v6.12.5 - 2018-01-11
|
||||
|
||||
* Fix breakage in deploy command from recent TS conversion #754 [Tim Perry]
|
||||
|
||||
## v6.12.4 - 2018-01-10
|
||||
|
||||
* Start using Prettier #753 [Tim Perry]
|
||||
|
||||
## v6.12.3 - 2018-01-09
|
||||
|
||||
* Lint TypeScript and CoffeeScript with resin-lint #743 [Tim Perry]
|
||||
* Move documentation generation to TypeScript #743 [Tim Perry]
|
||||
* Convert most of utils to TypeScript #743 [Tim Perry]
|
||||
|
||||
## v6.12.2 - 2018-01-09
|
||||
|
||||
* Convert windows paths to posix when passing to tar #748 [Andrew Shirley]
|
||||
|
||||
## v6.12.1 - 2018-01-02
|
||||
|
||||
* Fix deprecation warning for os configure, when passing a bare UUID #744 [Tim Perry]
|
||||
|
||||
## v6.12.0 - 2017-12-19
|
||||
|
||||
* Add ssh option for direct host OS access #737 [Andreas Fitzek]
|
||||
|
||||
## v6.11.0 - 2017-12-18
|
||||
|
||||
* Fix docs generation when building on windows #729 [Tim Perry]
|
||||
* Autodeploy built standalone binaries for all platforms to github #729 [Tim Perry]
|
||||
* Package the CLI into a standalone runnable binary #729 [Tim Perry]
|
||||
* Move from open to opn #729 [Tim Perry]
|
||||
|
||||
## v6.10.3 - 2017-12-15
|
||||
|
||||
* Ensure logout works even with invalid credentials, or if not logged in #730 [Tim Perry]
|
||||
|
||||
## v6.10.2 - 2017-11-27
|
||||
|
||||
* Inline the entire resin-cli-auth module #721 [Tim Perry]
|
||||
|
65
CONTRIBUTING.md
Normal file
65
CONTRIBUTING.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Contributing
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
After cloning this repository and running `npm install`, the CLI can be built with `npm run build`
|
||||
and executed with `./bin/run`. In order to ease development:
|
||||
|
||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||
* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly.
|
||||
|
||||
Before opening a PR, please be sure to test your changes with `npm test`.
|
||||
|
||||
## Semantic versioning and commit messages
|
||||
|
||||
The CLI version numbering adheres to [Semantic Versioning](http://semver.org/). The following
|
||||
header/row is required in the body of a commit message, and will cause the CI build to fail if absent:
|
||||
|
||||
```
|
||||
Change-type: patch|minor|major
|
||||
```
|
||||
|
||||
Version numbers and commit messages are automatically added to the `CHANGELOG.md` file by the CI
|
||||
build flow, after a pull request is merged. It should not be manually edited.
|
||||
|
||||
## Editing documentation files (CHANGELOG, README, website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
|
||||
Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
|
||||
* Selected sections of the README file.
|
||||
* The CLI's command documentation in source code (both Capitano and oclif commands), for example:
|
||||
* `lib/actions/build.coffee`
|
||||
* `lib/actions-oclif/env/add.ts`
|
||||
|
||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
||||
`doc/cli.markdown` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
The `INSTALL.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Windows
|
||||
|
||||
Please note that `npm run build:installer` (which generates the `.exe` executable installer on
|
||||
Windows) requires [MSYS2](https://www.msys2.org/) to be installed. Other than that, the standard
|
||||
Command Prompt or PowerShell can be used.
|
||||
|
||||
## TypeScript vs CoffeeScript, and Capitano vs oclif
|
||||
|
||||
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to
|
||||
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
|
||||
typing and formal programming interfaces. The migration is taking place gradually, as part of
|
||||
maintenance work or the implementation of new features.
|
||||
|
||||
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
|
||||
framework, but we recently decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
as native installers for Windows, macOS and Linux, and support for custom flag parsing (for
|
||||
example, we're still battling with Capitano's behavior of dropping leading zeros of arguments that
|
||||
look like integers such as some abbreviated UUIDs, and migrating to oclif is a solution). Again the
|
||||
migration is taking place gradually, with some CLI commands parsed by oclif and others by Capitano
|
||||
(a simple command line pre-parsing takes place in `app.ts` to decide whether to route full parsing
|
||||
to Capitano or oclif).
|
158
INSTALL.md
Normal file
158
INSTALL.md
Normal file
@ -0,0 +1,158 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method, using the traditional
|
||||
graphical desktop application installers for Windows and macOS (coming soon for Linux users too).
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them. Recommended for scripted installation in CI (continuous integration)
|
||||
environments.
|
||||
* [NPM Installation](#npm-installation): recommended for developers who may be interested in
|
||||
integrating the balena CLI in their existing Node.js projects or workflow.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
> **Windows users:** 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!
|
||||
|
||||
## Executable Installer
|
||||
|
||||
_Please note: the executable installers are in **beta** status (recently introduced)._
|
||||
|
||||
1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with "installer-BETA", for example:
|
||||
`balena-cli-v10.13.6-windows-x64-installer-BETA.exe`
|
||||
`balena-cli-v10.13.6-macOS-x64-installer-BETA.pkg`
|
||||
2. Double click to run. Your system may raise a pop-up warning that the installer is from an
|
||||
"unknown publisher" or "unidentified developer". Check the following instructions for how
|
||||
to get through the warnings:
|
||||
[Windows](https://github.com/balena-io/balena-cli/issues/1250) or
|
||||
[macOS](https://github.com/balena-io/balena-cli/issues/1251).
|
||||
(We are looking at how to get the installers digitally signed to avoid the warnings.)
|
||||
|
||||
After the installation completes, close and re-open any open command terminal windows so that the
|
||||
changes made by the installer to the PATH environment variable can take effect. Check that the
|
||||
installation was successful by running these commands:
|
||||
|
||||
* `balena` - should print the balena CLI help
|
||||
* `balena version` - should print the installed CLI version
|
||||
|
||||
> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
|
||||
> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
|
||||
> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
|
||||
> instructions for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with the word "standalone", for example:
|
||||
`balena-cli-v10.13.6-linux-x64-standalone.zip`
|
||||
`balena-cli-v10.13.6-macOS-x64-standalone.zip`
|
||||
`balena-cli-v10.13.6-windows-x64-standalone.zip`
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
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 8 or 10 (v12 has not been thoroughly tested yet)
|
||||
* Python 2.7
|
||||
* g++ compiler
|
||||
* make
|
||||
* git
|
||||
* On Windows, the `windows-build-tools` npm package should be installed too, running the following
|
||||
command in an administrator console (available as "Command Prompt (Admin)" or "Windows PowerShell
|
||||
(Admin)" when typing Windows+X):
|
||||
`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).
|
||||
|
||||
On some Linux distributions like Ubuntu, users often report permission or otherwise mysterious
|
||||
errors when using the system Node / npm packages installed via "apt-get". We suggest using
|
||||
[nvm](https://github.com/creationix/nvm) instead. Check this sample Dockerfile for installing the
|
||||
CLI on an Ubuntu Docker image: https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44
|
||||
|
||||
## 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.
|
133
README.md
133
README.md
@ -1,98 +1,91 @@
|
||||
Resin CLI
|
||||
=========
|
||||
# balena CLI
|
||||
|
||||
> The official resin.io CLI tool.
|
||||
The official balena CLI tool.
|
||||
|
||||
[](http://badge.fury.io/js/resin-cli)
|
||||
[](https://david-dm.org/resin-io/resin-cli)
|
||||
[](https://gitter.im/resin-io/chat)
|
||||
[](http://badge.fury.io/js/balena-cli)
|
||||
[](https://david-dm.org/balena-io/balena-cli)
|
||||
[](https://gitter.im/balena-io/chat)
|
||||
|
||||
Requisites
|
||||
----------
|
||||
## About
|
||||
|
||||
- [NodeJS](https://nodejs.org) (>= v4)
|
||||
- [Git](https://git-scm.com)
|
||||
- The following executables should be correctly installed in your shell environment:
|
||||
- `ssh`: Any recent version of the OpenSSH ssh client (required by `resin sync` and `resin ssh`)
|
||||
- if you need `ssh` to work behind the proxy you also need [`proxytunnel`](http://proxytunnel.sourceforge.net/) installed (available as `proxytunnel` package for Ubuntu, for example)
|
||||
- `rsync`: >= 2.6.9 (required by `resin sync`)
|
||||
The balena CLI (Command-Line Interface) allows you to interact with the balenaCloud and the
|
||||
[balena API](https://www.balena.io/docs/reference/api/overview/) through a terminal window
|
||||
on Linux, macOS or Windows. You can also write shell scripts around it, or import its Node.js
|
||||
modules to use it programmatically.
|
||||
As an [open-source project on GitHub](https://github.com/balena-io/balena-cli/), your contribution
|
||||
is also welcome!
|
||||
|
||||
##### Windows Support
|
||||
## Installation
|
||||
|
||||
Before installing resin-cli, you'll need a working node-gyp environment. If you don't already have one you'll see native module build errors during installation. To fix this, run `npm install -g --production windows-build-tools` in an administrator console (available as 'Command Prompt (Admin)' when pressing windows+x in Windows 7+).
|
||||
Check the [balena CLI installation instructions on GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
`resin sync` and `resin ssh` have not been thoroughly tested on the standard Windows cmd.exe shell. We recommend using bash (or a similar) shell, like Bash for Windows 10 or [Git for Windows](https://git-for-windows.github.io/).
|
||||
## Getting Started
|
||||
|
||||
If you still want to use `cmd.exe` you will have to use a package manager like MinGW or chocolatey. For MinGW the steps are:
|
||||
### Choosing a shell (command prompt/terminal)
|
||||
|
||||
1. Install [MinGW](http://www.mingw.org).
|
||||
2. Install the `msys-rsync` and `msys-openssh` packages.
|
||||
3. Add MinGW to the `%PATH%` if this hasn't been done by the installer already. The location where the binaries are places is usually `C:\MinGW\msys\1.0\bin`, but it can vary if you selected a different location in the installer.
|
||||
4. Copy your SSH keys to `%homedrive%%homepath\.ssh`.
|
||||
5. If you need `ssh` to work behind the proxy you also need to install [proxytunnel](http://proxytunnel.sourceforge.net/)
|
||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||
[PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6)
|
||||
are supported. We are aware of users also having a good experience with alternative shells,
|
||||
including:
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
* 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/)
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS) and [MSYS2](https://www.msys2.org/) (install the
|
||||
`msys-rsync` and `msys-openssh` packages too)
|
||||
|
||||
### Install
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
This might require elevated privileges in some environments.
|
||||
### Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ npm install --global --production resin-cli
|
||||
$ balena login
|
||||
```
|
||||
|
||||
### Login
|
||||
### Proxy support
|
||||
|
||||
```sh
|
||||
$ resin login
|
||||
```
|
||||
HTTP(S) proxies can be configured through any of the following methods, in order of preference:
|
||||
|
||||
_(Typically useful, but not strictly required for all commands)_
|
||||
* Set the `BALENARC_PROXY` environment variable in URL format (with protocol, host, port, and
|
||||
optionally basic auth).
|
||||
* Alternatively, use the [balena config file](https://www.npmjs.com/package/balena-settings-client#documentation)
|
||||
(project-specific or user-level) and set the `proxy` setting. It can be:
|
||||
* A string in URL format, or
|
||||
* An object in the [global-tunnel-ng options format](https://www.npmjs.com/package/global-tunnel-ng#options) (which allows more control).
|
||||
* Alternatively, set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
|
||||
### Run commands
|
||||
To get a proxy to work with the `balena ssh` command, check the
|
||||
[installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
Take a look at the full command documentation at [https://docs.resin.io/tools/cli/](https://docs.resin.io/tools/cli/#table-of-contents
|
||||
), or by running `resin help`.
|
||||
## Command reference documentation
|
||||
|
||||
---
|
||||
The full CLI command reference is available [on the web](https://www.balena.io/docs/reference/cli/
|
||||
) or by running `balena help` and `balena help --verbose`.
|
||||
|
||||
Plugins
|
||||
-------
|
||||
## Support, FAQ and troubleshooting
|
||||
|
||||
The Resin CLI can be extended with plugins to automate laborious tasks and overall provide a better experience when working with Resin.io. Check the [plugin development tutorial](https://github.com/resin-io/resin-plugin-hello) to learn how to build your own!
|
||||
If you come across any problems or would like to get in touch:
|
||||
|
||||
FAQ
|
||||
---
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md).
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud).
|
||||
* For bug reports or feature requests,
|
||||
[have a look at the GitHub issues or create a new one](https://github.com/balena-io/balena-cli/issues/).
|
||||
|
||||
### Where is my configuration file?
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
The per-user configuration file lives in `$HOME/.resinrc.yml` or `%UserProfile%\_resinrc.yml`, in Unix based operating systems and Windows respectively.
|
||||
Please have a look at the [CONTRIBUTING.md](./CONTRIBUTING.md) file for some guidance before
|
||||
submitting a pull request or updating documentation (because some files are automatically
|
||||
generated). Thank you for your help and interest!
|
||||
|
||||
The Resin CLI also attempts to read a `resinrc.yml` file in the current directory, which takes precedence over the per-user configuration file.
|
||||
## License
|
||||
|
||||
### How do I point the Resin CLI to staging?
|
||||
|
||||
The easiest way is to set the `RESINRC_RESIN_URL=resinstaging.io` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `resinUrl: resinstaging.io` to persist this setting.
|
||||
|
||||
### How do I make the Resin CLI persist data in another directory?
|
||||
|
||||
The Resin CLI persists your session token, as well as cached images in `$HOME/.resin` or `%UserProfile%\_resin`.
|
||||
|
||||
Pointing the Resin CLI to persist data in another location is necessary in certain environments, like a server, where there is no home directory, or a device running resinOS, which erases all data after a restart.
|
||||
|
||||
You can accomplish this by setting `RESINRC_DATA_DIRECTORY=/opt/resin` or adding `dataDirectory: /opt/resin` to your configuration file, replacing `/opt/resin` with your desired directory.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
If you're having any problems, check our [troubleshooting guide](https://github.com/resin-io/resin-cli/blob/master/TROUBLESHOOTING.md) and if your problem is not addressed there, please [raise an issue](https://github.com/resin-io/resin-cli/issues/new) on GitHub and the resin.io team will be happy to help.
|
||||
|
||||
You can also get in touch with us in the resin.io [forums](https://forums.resin.io/).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The project is licensed under the Apache 2.0 license.
|
||||
The project is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
A copy is also available in the LICENSE file in this repository.
|
||||
|
@ -1,15 +1,41 @@
|
||||
Troubleshooting
|
||||
===============
|
||||
# FAQ & Troubleshooting
|
||||
|
||||
This document contains common issues related to the Resin CLI, and how to fix them.
|
||||
This document contains some common issues, questions and answers related to the balena CLI.
|
||||
|
||||
### After burning to an sdcard, my device doesn't boot
|
||||
## Where is my configuration file?
|
||||
|
||||
The per-user configuration file lives in `$HOME/.balenarc.yml` or `%UserProfile%\_balenarc.yml`, in
|
||||
Unix based operating systems and Windows respectively.
|
||||
|
||||
The balena CLI also attempts to read a `balenarc.yml` file in the current directory, which takes
|
||||
precedence over the per-user configuration file.
|
||||
|
||||
## How do I point the balena CLI to staging?
|
||||
|
||||
The easiest way is to set the `BALENARC_BALENA_URL=balena-staging.com` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `balenaUrl: balena-staging.com` to
|
||||
persist this setting.
|
||||
|
||||
## How do I make the balena CLI persist data in another directory?
|
||||
|
||||
The balena CLI persists your session token, as well as cached images in `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`.
|
||||
|
||||
Pointing the balena CLI to persist data in another location is necessary in certain environments,
|
||||
like a server, where there is no home directory, or a device running balenaOS, which erases all
|
||||
data after a restart.
|
||||
|
||||
You can accomplish this by setting `BALENARC_DATA_DIRECTORY=/opt/balena` or adding `dataDirectory:
|
||||
/opt/balena` to your configuration file, replacing `/opt/balena` with your desired directory.
|
||||
|
||||
## After burning to an sdcard, my device doesn't boot
|
||||
|
||||
- The downloaded image is not complete (download was interrupted).
|
||||
|
||||
Please clean the cache (`%HOME/.resin/cache` or `C:\Users\<user>\_resin\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
Please clean the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
|
||||
### I get a permission error when burning to an sdcard
|
||||
## I get a permission error when burning to an sdcard
|
||||
|
||||
- The SDCard is locked.
|
||||
|
||||
@ -24,36 +50,36 @@ net.js:156
|
||||
Error: EINVAL, invalid argument
|
||||
at new Socket (net.js:156:18)
|
||||
at process.stdin (node.js:664:19)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
```
|
||||
|
||||
- Some interactive widgets don't work on `Cygwin`. If you're running Windows, it's preferrable that you use `cmd.exe`, as `Cygwin` is [not official supported by Node.js](https://github.com/chjj/blessed/issues/56#issuecomment-42671945).
|
||||
|
||||
### I get `Invalid MBR boot signature` when configuring a device
|
||||
## I get `Invalid MBR boot signature` when configuring a device
|
||||
|
||||
This error, accompanied with something like: `Expected 0xAA55, but saw 0x29FE` usually indicates a corrupted device operating system image in the cache, due to bad a internet connection during the download process.
|
||||
|
||||
Try clearing the cache with the following command and trying again:
|
||||
|
||||
```sh
|
||||
$ rm -rf $HOME/.resin/cache
|
||||
$ rm -rf $HOME/.balena/cache
|
||||
```
|
||||
|
||||
Or in Windows:
|
||||
|
||||
```sh
|
||||
> del /s /q %UserProfile%\_resin\cache
|
||||
> del /s /q %UserProfile%\_balena\cache
|
||||
```
|
||||
|
||||
### I get `EACCES: permission denied` when logging in
|
||||
## I get `EACCES: permission denied` when logging in
|
||||
|
||||
The Resin CLI stores the session token in `$HOME/.resin` or `C:\Users\<user>\_resin` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the Resin CLI as `root`, and thus the directory got owned by him.
|
||||
The balena CLI stores the session token in `$HOME/.balena` or `C:\Users\<user>\_balena` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the balena CLI as `root`, and thus the directory got owned by him.
|
||||
|
||||
Try resetting the ownership by running:
|
||||
|
||||
```sh
|
||||
$ sudo chown -R <user> $HOME/.resin
|
||||
$ sudo chown -R <user> $HOME/.balena
|
||||
```
|
||||
|
43
appveyor.yml
Normal file
43
appveyor.yml
Normal file
@ -0,0 +1,43 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%AppData%\npm-cache'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
# what combinations to test
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm config set python 'C:\Python27\python.exe'
|
||||
- npm --version
|
||||
# - npm install
|
||||
|
||||
build: off
|
||||
test: off
|
||||
deploy: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm test
|
||||
|
||||
deploy_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
# - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
# - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
282
automation/build-bin.ts
Normal file
282
automation/build-bin.ts
Normal file
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile, spawn } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
import { exec as execPkg } from 'pkg';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
import * as util from 'util';
|
||||
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
// Note: the following 'tslint disable' line was only required to
|
||||
// satisfy ts-node under Appveyor's MSYS2 on Windows -- oddly specific.
|
||||
// Maybe something to do with '/' vs '\' in paths in some tslint file.
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
export const packageJSON = require(path.join(ROOT, 'package.json'));
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
|
||||
function dPath(...paths: string[]) {
|
||||
return path.join(ROOT, 'dist', ...paths);
|
||||
}
|
||||
|
||||
interface PathByPlatform {
|
||||
[platform: string]: string;
|
||||
}
|
||||
|
||||
const standaloneZips: PathByPlatform = {
|
||||
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||
};
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-installer-BETA.pkg`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-installer-BETA.exe`),
|
||||
};
|
||||
|
||||
export const finalReleaseAssets: { [platform: string]: string[] } = {
|
||||
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
|
||||
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
|
||||
linux: [standaloneZips['linux']],
|
||||
};
|
||||
|
||||
const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', code => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the 'pkg' module to create a single large executable file with
|
||||
* the contents of 'node_modules' and the CLI's javascript code.
|
||||
* Also copy a number of native modules (binary '.node' files) that are
|
||||
* compiled during 'npm install' to the 'build-bin' folder, alongside
|
||||
* the single large executable file created by pkg. (This is necessary
|
||||
* because of a pkg limitation that does not allow binary executables
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
const args = [
|
||||
'--target',
|
||||
'node10',
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
];
|
||||
console.log('=======================================================');
|
||||
console.log(`execPkg ${args.join(' ')}`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
|
||||
await execPkg(args);
|
||||
|
||||
const xpaths: Array<[string, string[]]> = [
|
||||
// [platform, [path, to, file]]
|
||||
['*', ['opn', 'xdg-open']],
|
||||
['darwin', ['denymount', 'bin', 'denymount']],
|
||||
];
|
||||
await Bluebird.map(xpaths, ([platform, xpath]) => {
|
||||
if (platform === '*' || platform === process.platform) {
|
||||
// eg copy from node_modules/opn/xdg-open to build-bin/xdg-open
|
||||
return fs.copy(
|
||||
path.join(ROOT, 'node_modules', ...xpath),
|
||||
path.join(ROOT, 'build-bin', xpath.pop()!),
|
||||
);
|
||||
}
|
||||
});
|
||||
const nativeExtensionPaths: string[] = await filehound
|
||||
.create()
|
||||
.paths(path.join(ROOT, 'node_modules'))
|
||||
.ext(['node', 'dll'])
|
||||
.find();
|
||||
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
||||
|
||||
await Bluebird.map(nativeExtensionPaths, extPath =>
|
||||
fs.copy(
|
||||
extPath,
|
||||
extPath.replace(
|
||||
path.join(ROOT, 'node_modules'),
|
||||
path.join(ROOT, 'build-bin'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the zip file for the standalone 'pkg' bundle previously created
|
||||
* by the buildPkg() function in 'build-bin.ts'.
|
||||
*/
|
||||
async function zipPkg() {
|
||||
const outputFile = standaloneZips[process.platform];
|
||||
if (!outputFile) {
|
||||
throw new Error(
|
||||
`Standalone installer unavailable for platform "${process.platform}"`,
|
||||
);
|
||||
}
|
||||
await fs.mkdirp(path.dirname(outputFile));
|
||||
await new Promise((resolve, reject) => {
|
||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 7 },
|
||||
});
|
||||
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
|
||||
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
outputStream.on('close', resolve);
|
||||
outputStream.on('error', reject);
|
||||
|
||||
archive.on('error', reject);
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
await buildPkg();
|
||||
await zipPkg();
|
||||
} catch (error) {
|
||||
console.log(`Error creating standalone zip package: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Standalone zip package build completed`);
|
||||
}
|
||||
|
||||
async function renameInstallerFiles() {
|
||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||
await fs.rename(
|
||||
oclifInstallers[process.platform],
|
||||
renamedOclifInstallers[process.platform],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||
*/
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
'-f',
|
||||
exeName,
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
*/
|
||||
export async function buildOclifInstaller() {
|
||||
let packOS = '';
|
||||
let packOpts = ['-r', ROOT];
|
||||
if (process.platform === 'darwin') {
|
||||
packOS = 'macos';
|
||||
} else if (process.platform === 'win32') {
|
||||
packOS = 'win';
|
||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||
}
|
||||
if (packOS) {
|
||||
console.log(`Building oclif installer for CLI ${version}`);
|
||||
const packCmd = `pack:${packOS}`;
|
||||
const dirs = [path.join(ROOT, 'dist', packOS)];
|
||||
if (packOS === 'win') {
|
||||
dirs.push(path.join(ROOT, 'tmp', 'win*'));
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback(cb => rimraf(dir, cb));
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
await renameInstallerFiles();
|
||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||
// The macOS installer is automatically signed by oclif (which runs the
|
||||
// `pkgbuild` tool), using the certificate name given in package.json
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
150
automation/capitanodoc/capitanodoc.ts
Normal file
150
automation/capitanodoc/capitanodoc.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { MarkdownFileParser } from './utils';
|
||||
|
||||
/**
|
||||
* This is the skeleton of CLI documentation/reference web page at:
|
||||
* https://www.balena.io/docs/reference/cli/
|
||||
*
|
||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||
* some content to this object.
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'Balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/actions/api-key.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: ['build/actions/app.js'],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: ['build/actions/auth.js'],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: ['build/actions/device.js'],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/actions/environment-variables.js',
|
||||
'build/actions-oclif/env/add.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: ['build/actions/tags.js'],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['build/actions/help.js', 'build/actions-oclif/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: ['build/actions/keys.js'],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/actions/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/actions/scan.js',
|
||||
'build/actions/ssh.js',
|
||||
'build/actions/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions/notes.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: ['build/actions/os.js'],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: ['build/actions/config.js'],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/actions/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/actions/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: ['build/actions/local/index.js'],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/actions/build.js', 'build/actions/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/actions/join.js', 'build/actions/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/actions/util.js'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify and return the `capitanoDoc` object above in order to render the
|
||||
* CLI documentation/reference web page at:
|
||||
* https://www.balena.io/docs/reference/cli/
|
||||
*
|
||||
* This function parses the README.md file to extract relevant sections
|
||||
* for the documentation web page.
|
||||
*/
|
||||
export async function getCapitanoDoc(): Promise<typeof capitanoDoc> {
|
||||
const readmePath = path.join(__dirname, '..', '..', 'README.md');
|
||||
const mdParser = new MarkdownFileParser(readmePath);
|
||||
const sections: string[] = await Promise.all([
|
||||
mdParser.getSectionOfTitle('About').then((sectionLines: string) => {
|
||||
// delete the title of the 'About' section for the web page
|
||||
const match = /^(#+)\s+.+?\n\s*([^]*)/.exec(sectionLines);
|
||||
if (!match || match.length < 3) {
|
||||
throw new Error(`Error parsing section title`);
|
||||
}
|
||||
// match[1] has the title, match[2] has the rest
|
||||
return match && match[2];
|
||||
}),
|
||||
mdParser.getSectionOfTitle('Installation'),
|
||||
mdParser.getSectionOfTitle('Getting Started'),
|
||||
mdParser.getSectionOfTitle('Support, FAQ and troubleshooting'),
|
||||
]);
|
||||
capitanoDoc.introduction = sections.join('\n');
|
||||
return capitanoDoc;
|
||||
}
|
33
automation/capitanodoc/doc-types.d.ts
vendored
Normal file
33
automation/capitanodoc/doc-types.d.ts
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import { CommandDefinition as CapitanoCommand } from 'capitano';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
export interface Document {
|
||||
title: string;
|
||||
introduction: string;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
title: string;
|
||||
commands: Array<CapitanoCommand | OclifCommand>;
|
||||
}
|
||||
|
||||
export { CapitanoCommand, OclifCommand };
|
89
automation/capitanodoc/index.ts
Normal file
89
automation/capitanodoc/index.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
import { getCapitanoDoc } from './capitanodoc';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as markdown from './markdown';
|
||||
|
||||
/**
|
||||
* Generates the markdown document (as a string) for the CLI documentation
|
||||
* page on the web: https://www.balena.io/docs/reference/cli/
|
||||
*/
|
||||
export async function renderMarkdown(): Promise<string> {
|
||||
const capitanodoc = await getCapitanoDoc();
|
||||
const result: Document = {
|
||||
title: capitanodoc.title,
|
||||
introduction: capitanodoc.introduction,
|
||||
categories: [],
|
||||
};
|
||||
|
||||
for (const commandCategory of capitanodoc.categories) {
|
||||
const category: Category = {
|
||||
title: commandCategory.title,
|
||||
commands: [],
|
||||
};
|
||||
|
||||
for (const jsFilename of commandCategory.files) {
|
||||
category.commands.push(
|
||||
...(jsFilename.includes('actions-oclif')
|
||||
? importOclifCommands(jsFilename)
|
||||
: importCapitanoCommands(jsFilename)),
|
||||
);
|
||||
}
|
||||
result.categories.push(category);
|
||||
}
|
||||
|
||||
return markdown.render(result);
|
||||
}
|
||||
|
||||
function importCapitanoCommands(jsFilename: string): CapitanoCommand[] {
|
||||
const actions = require(path.join(process.cwd(), jsFilename));
|
||||
const commands: CapitanoCommand[] = [];
|
||||
|
||||
if (actions.signature) {
|
||||
commands.push(_.omit(actions, 'action'));
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action'));
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
return [command];
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the CLI docs markdown to stdout.
|
||||
* See package.json for how the output is redirected to a file.
|
||||
*/
|
||||
async function printMarkdown() {
|
||||
try {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
printMarkdown();
|
152
automation/capitanodoc/markdown.ts
Normal file
152
automation/capitanodoc/markdown.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result.push('### Options');
|
||||
|
||||
for (const option of command.options!) {
|
||||
if (option == null) {
|
||||
throw new Error(`Undefined option in markdown generation!`);
|
||||
}
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage)}`];
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
.join('\n')
|
||||
.trim();
|
||||
result.push(description);
|
||||
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.flags)) {
|
||||
result.push('### Options');
|
||||
for (const [name, flag] of Object.entries(command.flags!)) {
|
||||
if (name === 'help') {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
result.push(`#### ${flagUsage}`);
|
||||
result.push(flag.description || '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result.push(
|
||||
...(typeof command === 'object'
|
||||
? renderCapitanoCommand(command)
|
||||
: renderOclifCommand(command)),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAnchor(cmdSignature: string): string {
|
||||
return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function renderToc(categories: Category[]): string[] {
|
||||
const result = [`# CLI Command Reference`];
|
||||
|
||||
for (const category of categories) {
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map(command => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
: utils.capitanoizeOclifUsage(command.usage); // oclif
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<CapitanoCommand | OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: CapitanoCommand | OclifCommand, x: string) =>
|
||||
typeof cmd === 'object' // Capitano vs oclif command
|
||||
? cmd.signature.replace(/\W+/g, ' ').includes(x)
|
||||
: (cmd.usage || '')
|
||||
.toString()
|
||||
.replace(/\W+/g, ' ')
|
||||
.includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
...renderToc(doc.categories),
|
||||
];
|
||||
for (const category of doc.categories) {
|
||||
result.push(...renderCategory(category));
|
||||
}
|
||||
return result.join('\n\n');
|
||||
}
|
149
automation/capitanodoc/utils.ts
Normal file
149
automation/capitanodoc/utils.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OptionDefinition } from 'capitano';
|
||||
import * as ent from 'ent';
|
||||
import * as fs from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as readline from 'readline';
|
||||
|
||||
export function getOptionPrefix(signature: string) {
|
||||
if (signature.length > 1) {
|
||||
return '--';
|
||||
} else {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionSignature(signature: string) {
|
||||
return `${getOptionPrefix(signature)}${signature}`;
|
||||
}
|
||||
|
||||
export function parseCapitanoOption(option: OptionDefinition): string {
|
||||
let result = getOptionSignature(option.signature);
|
||||
|
||||
if (_.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (_.isString(option.alias)) {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
if (option.parameter) {
|
||||
result += ` <${option.parameter}>`;
|
||||
}
|
||||
|
||||
return ent.encode(result);
|
||||
}
|
||||
|
||||
/** Convert e.g. 'env add NAME [VALUE]' to 'env add <name> [value]' */
|
||||
export function capitanoizeOclifUsage(
|
||||
oclifUsage: string | string[] | undefined,
|
||||
): string {
|
||||
return (oclifUsage || '')
|
||||
.toString()
|
||||
.replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, match => `<${match}>`)
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export class MarkdownFileParser {
|
||||
constructor(public mdFilePath: string) {}
|
||||
|
||||
/**
|
||||
* Extract the lines of a markdown document section with the given title.
|
||||
* For example, consider this sample markdown document:
|
||||
* ```
|
||||
* # balena CLI
|
||||
*
|
||||
* ## Introduction
|
||||
* Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
*
|
||||
* ## Getting Started
|
||||
* sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
*
|
||||
* ### Prerequisites
|
||||
* - Foo
|
||||
* - Bar
|
||||
*
|
||||
* ## Support
|
||||
* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
|
||||
* ```
|
||||
*
|
||||
* Calling getSectionOfTitle('Getting Started') for the markdown doc above
|
||||
* returns everything from line '## Getting Started' (included) to line
|
||||
* '## Support' (excluded). This method counts the number of '#' characters
|
||||
* to determine that subsections should be included as part of the parent
|
||||
* section.
|
||||
*
|
||||
* @param title The section title without '#' chars, eg. 'Getting Started'
|
||||
*/
|
||||
public async getSectionOfTitle(
|
||||
title: string,
|
||||
includeSubsections = true,
|
||||
): Promise<string> {
|
||||
let foundSectionLines: string[];
|
||||
let foundSectionLevel = 0;
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(this.mdFilePath),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
rl.on('line', line => {
|
||||
// try to match a line like "## Getting Started", where the number
|
||||
// of '#' characters is the sectionLevel ('##' -> 2), and the
|
||||
// sectionTitle is "Getting Started"
|
||||
const match = /^(#+)\s+(.+)/.exec(line);
|
||||
if (match) {
|
||||
const sectionLevel = match[1].length;
|
||||
const sectionTitle = match[2];
|
||||
|
||||
// If the target section had already been found: append a line, or end it
|
||||
if (foundSectionLines) {
|
||||
if (!includeSubsections || sectionLevel <= foundSectionLevel) {
|
||||
// end previously found section
|
||||
rl.close();
|
||||
}
|
||||
} else if (sectionTitle === title) {
|
||||
// found the target section
|
||||
foundSectionLevel = sectionLevel;
|
||||
foundSectionLines = [];
|
||||
}
|
||||
}
|
||||
if (foundSectionLines) {
|
||||
foundSectionLines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
rl.on('close', () => {
|
||||
if (foundSectionLines) {
|
||||
resolve(foundSectionLines.join('\n'));
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${
|
||||
this.mdFilePath
|
||||
}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
59
automation/deploy-bin.ts
Normal file
59
automation/deploy-bin.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as publishRelease from 'publish-release';
|
||||
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
|
||||
/**
|
||||
* Create or update a release in GitHub's releases page, uploading the
|
||||
* installer files (standalone zip + native oclif installers).
|
||||
*/
|
||||
export async function createGitHubRelease() {
|
||||
console.log(`Publishing release ${version} to GitHub`);
|
||||
const ghRelease = await Bluebird.fromCallback(
|
||||
publishRelease.bind(null, {
|
||||
token: GITHUB_TOKEN || '',
|
||||
owner: 'balena-io',
|
||||
repo: 'balena-cli',
|
||||
tag: version,
|
||||
name: `balena-CLI ${version}`,
|
||||
reuseRelease: true,
|
||||
assets: finalReleaseAssets[process.platform],
|
||||
}),
|
||||
);
|
||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level function to create a CLI release in GitHub's releases page:
|
||||
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
||||
* display on the releases page, and call createGitHubRelease() to upload
|
||||
* the files.
|
||||
*/
|
||||
export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
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,
|
||||
fixPathForMsys,
|
||||
ROOT,
|
||||
runUnderMsys,
|
||||
} from './build-bin';
|
||||
import { release } from './deploy-bin';
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
* 'build:installer' (to build a native oclif installer)
|
||||
* 'build:standalone' (to build a standalone pkg package)
|
||||
* 'release' (to create/update a GitHub release)
|
||||
*
|
||||
* In the case of 'build:installer', also call runUnderMsys() to switch the
|
||||
* shell from cmd.exe to MSYS2 bash.exe.
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
console.error('Error: missing args');
|
||||
process.exit(1);
|
||||
}
|
||||
const commands: { [cmd: string]: () => void } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
throw new Error(`Error: unknown build target: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If runUnderMsys() is called to re-execute this script under MSYS2,
|
||||
// the current working dir becomes the MSYS2 homedir, so we change back.
|
||||
process.chdir(ROOT);
|
||||
|
||||
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||
// (patched) to copy/extract the CLI files, run npm install and then
|
||||
// create the NSIS executable installer for Windows. This was necessary
|
||||
// to avoid issues with a 260-char limit on Windows paths (possibly a
|
||||
// limitation of some library used by NSIS), as the "current working dir"
|
||||
// provided by balena CI is a rather long path to start with.
|
||||
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
|
||||
const randID = require('crypto')
|
||||
.randomBytes(6)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_'); // base64url (RFC 4648)
|
||||
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
try {
|
||||
if (arg === 'build:installer' && process.platform === 'win32') {
|
||||
// ensure running under MSYS2
|
||||
if (!process.env.MSYSTEM) {
|
||||
process.env.MSYS2_PATH_TYPE = 'inherit';
|
||||
await runUnderMsys([
|
||||
fixPathForMsys(process.argv[0]),
|
||||
fixPathForMsys(process.argv[1]),
|
||||
arg,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
|
||||
throw new Error(
|
||||
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
|
||||
);
|
||||
}
|
||||
}
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
console.log(`Error running command "${arg}": ${err}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
19
automation/tsconfig.json
Normal file
19
automation/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"preserveConstEnums": true,
|
||||
"removeComments": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots" : [
|
||||
"../node_modules/@types",
|
||||
"../typings"
|
||||
]
|
||||
}
|
||||
}
|
73
balena-completion.bash
Normal file
73
balena-completion.bash
Normal file
@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
scan ssh util version whoami"
|
||||
# Sub-completions
|
||||
app_cmds="create restart rm"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="identify init move public-url reboot register rename rm \
|
||||
shutdown"
|
||||
device_public_url_cmds="disable enable status"
|
||||
env_cmds="add rename rm"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
util_cmds="available-drives"
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"app")
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
"config")
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
"device")
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
"env")
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
"key")
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
"local")
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
"os")
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
"util")
|
||||
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
elif [ $COMP_CWORD -eq 3 ]
|
||||
then
|
||||
case "$prev" in
|
||||
"public-url")
|
||||
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
|
||||
;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
12
bin/balena
Executable file
12
bin/balena
Executable file
@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheFile: __dirname + '/.fast-boot.json'
|
||||
})
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
29
bin/balena-dev
Executable file
29
bin/balena-dev
Executable file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// ****************************************************************************
|
||||
// THIS IS FOR DEV PERROSES ONLY AND WILL NOT BE PART OF THE PUBLISHED PACKAGE
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
require('coffeescript/register');
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
// Note: before ts-node v6.0.0, 'transpile-only' (no type checking) was the
|
||||
// default option. We upgraded ts-node and found that adding 'transpile-only'
|
||||
// was necessary to avoid a mysterious 'null' error message. On the plus side,
|
||||
// it is supposed to run faster. We still benefit from type checking when
|
||||
// running 'npm run build'.
|
||||
require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../lib/app').run();
|
@ -1,115 +0,0 @@
|
||||
# coffeelint: disable=max_line_length
|
||||
|
||||
module.exports =
|
||||
title: 'Resin CLI Documentation'
|
||||
introduction: '''
|
||||
This tool allows you to interact with the resin.io api from the comfort of your command line.
|
||||
|
||||
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
|
||||
|
||||
To get started download the CLI from npm.
|
||||
|
||||
$ npm install resin-cli -g
|
||||
|
||||
Then authenticate yourself:
|
||||
|
||||
$ resin login
|
||||
|
||||
Now you have access to all the commands referenced below.
|
||||
|
||||
## Proxy support
|
||||
|
||||
The CLI does support HTTP(S) proxies.
|
||||
|
||||
You can configure the proxy using several methods (in order of their precedence):
|
||||
|
||||
* set the `RESINRC_PROXY` environment variable in the URL format (with protocol, host, port, and optionally the basic auth),
|
||||
* use the [resin config file](https://www.npmjs.com/package/resin-settings-client#documentation) (project-specific or user-level)
|
||||
and set the `proxy` setting. This can be:
|
||||
* a string in the URL format,
|
||||
* or an object following [this format](https://www.npmjs.com/package/global-tunnel-ng#options), which allows more control,
|
||||
* or set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
|
||||
environment variable (in the same standard URL format).
|
||||
'''
|
||||
|
||||
categories: [
|
||||
{
|
||||
title: 'Application'
|
||||
files: [ 'lib/actions/app.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [ 'lib/actions/auth.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [ 'lib/actions/device.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [ 'lib/actions/environment-variables.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Help',
|
||||
files: [ 'lib/actions/help.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Information',
|
||||
files: [ 'lib/actions/info.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [ 'lib/actions/keys.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: [ 'lib/actions/logs.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Sync',
|
||||
files: [ 'lib/actions/sync.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'SSH',
|
||||
files: [ 'lib/actions/ssh.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: [ 'lib/actions/notes.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [ 'lib/actions/os.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [ 'lib/actions/config.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: [ 'lib/actions/preload.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: [ 'lib/actions/settings.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Wizard',
|
||||
files: [ 'lib/actions/wizard.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [ 'lib/actions/local/index.coffee' ]
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: [
|
||||
'lib/actions/build.coffee'
|
||||
'lib/actions/deploy.coffee'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: [ 'lib/actions/util.coffee' ]
|
||||
},
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Provisioning Resin.io devices in automated (non-interactive) mode
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
@ -7,7 +7,7 @@ It requires collecting some preliminary information _once_.
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
resin device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
@ -20,15 +20,15 @@ But before you can run it you need to collect the parameters and build the confi
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
resin devices supported
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
1. `APP_ID`. Create an application (`resin app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`resin apps`) and notice its ID.
|
||||
1. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
resin os versions DEVICE_TYPE
|
||||
balena os versions DEVICE_TYPE
|
||||
```
|
||||
and pick the version that you need, like _v2.0.6+rev1.prod_.
|
||||
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
|
||||
@ -36,10 +36,10 @@ But before you can run it you need to collect the parameters and build the confi
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
resin util available-drives
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
The resin CLI will not display the system drives to protect you,
|
||||
The balena CLI will not display the system drives to protect you,
|
||||
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
|
||||
|
||||
Now we have all the parameters -- time to build the config file.
|
||||
@ -50,21 +50,21 @@ Interactive device provisioning process often includes collecting some extra dev
|
||||
|
||||
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
|
||||
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./resin-os/raspberrypi3-config.json_.
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
|
||||
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./resin-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
|
||||
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
resin os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
resin os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
```
|
||||
|
||||
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
|
||||
@ -97,11 +97,11 @@ There are several ways to eliminate it and make the process fully non-interactiv
|
||||
|
||||
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
|
||||
|
||||
But if you're using a machine dedicated to resin provisioning this can be fine, and also the simplest thing to do.
|
||||
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
You can configure the `resin` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
|
||||
### Extra initialization config
|
||||
|
||||
@ -109,4 +109,4 @@ As of June 2017 all the supported devices should not require any other interacti
|
||||
|
||||
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
|
||||
|
||||
If that is the case please raise the issue in the resin CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
||||
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
||||
|
1492
doc/cli.markdown
1492
doc/cli.markdown
File diff suppressed because it is too large
Load Diff
@ -1,46 +0,0 @@
|
||||
_ = require('lodash')
|
||||
path = require('path')
|
||||
capitanodoc = require('../../capitanodoc')
|
||||
markdown = require('./markdown')
|
||||
|
||||
result = {}
|
||||
result.title = capitanodoc.title
|
||||
result.introduction = capitanodoc.introduction
|
||||
result.categories = []
|
||||
|
||||
for commandCategory in capitanodoc.categories
|
||||
category = {}
|
||||
category.title = commandCategory.title
|
||||
category.commands = []
|
||||
|
||||
for file in commandCategory.files
|
||||
actions = require(path.join(process.cwd(), file))
|
||||
|
||||
if actions.signature?
|
||||
category.commands.push(_.omit(actions, 'action'))
|
||||
else
|
||||
for actionName, actionCommand of actions
|
||||
category.commands.push(_.omit(actionCommand, 'action'))
|
||||
|
||||
result.categories.push(category)
|
||||
|
||||
result.toc = _.cloneDeep(result.categories)
|
||||
result.toc = _.map result.toc, (category) ->
|
||||
category.commands = _.map category.commands, (command) ->
|
||||
return {
|
||||
signature: command.signature
|
||||
anchor: '#' + command.signature
|
||||
.replace(/\s/g,'-')
|
||||
.replace(/</g, '60-')
|
||||
.replace(/>/g, '-62-')
|
||||
.replace(/\[/g, '')
|
||||
.replace(/\]/g, '-')
|
||||
.replace(/--/g, '-')
|
||||
.replace(/\.\.\./g, '')
|
||||
.replace(/\|/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
return category
|
||||
|
||||
console.log(markdown.display(result))
|
@ -1,66 +0,0 @@
|
||||
_ = require('lodash')
|
||||
ent = require('ent')
|
||||
utils = require('./utils')
|
||||
|
||||
exports.command = (command) ->
|
||||
result = """
|
||||
## #{ent.encode(command.signature)}
|
||||
|
||||
#{command.help}\n
|
||||
"""
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
result += '\n### Options'
|
||||
|
||||
for option in command.options
|
||||
result += """
|
||||
\n\n#### #{utils.parseSignature(option)}
|
||||
|
||||
#{option.description}
|
||||
"""
|
||||
|
||||
result += '\n'
|
||||
|
||||
return result
|
||||
|
||||
exports.category = (category) ->
|
||||
result = """
|
||||
# #{category.title}\n
|
||||
"""
|
||||
|
||||
for command in category.commands
|
||||
result += '\n' + exports.command(command)
|
||||
|
||||
return result
|
||||
|
||||
exports.toc = (toc) ->
|
||||
result = '''
|
||||
# Table of contents\n
|
||||
'''
|
||||
|
||||
for category in toc
|
||||
|
||||
result += """
|
||||
\n- #{category.title}\n\n
|
||||
"""
|
||||
|
||||
for command in category.commands
|
||||
result += """
|
||||
\t- [#{ent.encode(command.signature)}](#{command.anchor})\n
|
||||
"""
|
||||
|
||||
return result
|
||||
|
||||
exports.display = (doc) ->
|
||||
result = """
|
||||
# #{doc.title}
|
||||
|
||||
#{doc.introduction}
|
||||
|
||||
#{exports.toc(doc.toc)}
|
||||
"""
|
||||
|
||||
for category in doc.categories
|
||||
result += '\n' + exports.category(category)
|
||||
|
||||
return result
|
@ -1,26 +0,0 @@
|
||||
_ = require('lodash')
|
||||
ent = require('ent')
|
||||
|
||||
exports.getOptionPrefix = (signature) ->
|
||||
if signature.length > 1
|
||||
return '--'
|
||||
else
|
||||
return '-'
|
||||
|
||||
exports.getOptionSignature = (signature) ->
|
||||
return "#{exports.getOptionPrefix(signature)}#{signature}"
|
||||
|
||||
exports.parseSignature = (option) ->
|
||||
result = exports.getOptionSignature(option.signature)
|
||||
|
||||
if not _.isEmpty(option.alias)
|
||||
if _.isString(option.alias)
|
||||
result += ", #{exports.getOptionSignature(option.alias)}"
|
||||
else
|
||||
for alias in option.alias
|
||||
result += ", #{exports.getOptionSignature(option.alias)}"
|
||||
|
||||
if option.parameter?
|
||||
result += " <#{option.parameter}>"
|
||||
|
||||
return ent.encode(result)
|
@ -1,15 +1,12 @@
|
||||
path = require('path')
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
coffeelint = require('gulp-coffeelint')
|
||||
inlinesource = require('gulp-inline-source')
|
||||
mocha = require('gulp-mocha')
|
||||
shell = require('gulp-shell')
|
||||
packageJSON = require('./package.json')
|
||||
|
||||
OPTIONS =
|
||||
config:
|
||||
coffeelint: path.join(__dirname, 'coffeelint.json')
|
||||
files:
|
||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||
app: 'lib/**/*.coffee'
|
||||
@ -23,28 +20,21 @@ gulp.task 'pages', ->
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages'))
|
||||
|
||||
gulp.task 'coffee', [ 'lint' ], ->
|
||||
gulp.task 'coffee', ->
|
||||
gulp.src(OPTIONS.files.app)
|
||||
.pipe(coffee(bare: true, header: true))
|
||||
.pipe(gulp.dest(OPTIONS.directories.build))
|
||||
|
||||
gulp.task 'lint', ->
|
||||
gulp.src(OPTIONS.files.coffee)
|
||||
.pipe(coffeelint({
|
||||
optFile: OPTIONS.config.coffeelint
|
||||
}))
|
||||
.pipe(coffeelint.reporter())
|
||||
|
||||
gulp.task 'test', ->
|
||||
gulp.src(OPTIONS.files.tests, read: false)
|
||||
.pipe(mocha({
|
||||
reporter: 'min'
|
||||
reporter: 'spec'
|
||||
}))
|
||||
|
||||
gulp.task 'build', [
|
||||
gulp.task 'build', gulp.series [
|
||||
'coffee',
|
||||
'pages'
|
||||
]
|
||||
|
||||
gulp.task 'watch', [ 'build' ], ->
|
||||
gulp.task 'watch', gulp.series [ 'build' ], ->
|
||||
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])
|
||||
|
150
lib/actions-oclif/env/add.ts
vendored
Normal file
150
lib/actions-oclif/env/add.ts
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an environment or config variable to an application or device.
|
||||
|
||||
Add an environment or config variable to an application or device, as selected
|
||||
by the respective command-line options.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use \`--quiet\` to suppress it.
|
||||
|
||||
Service-specific variables are not currently supported. The given command line
|
||||
examples variables that apply to all services in an app or device.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'environment or config variable name',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from CLI's environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
public static usage =
|
||||
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
|
||||
|
||||
public static flags = {
|
||||
application: flags.string({
|
||||
char: 'a',
|
||||
description: 'application name',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
device: flags.string({
|
||||
char: 'd',
|
||||
description: 'device UUID',
|
||||
exclusive: ['application'],
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
quiet: flags.boolean({
|
||||
char: 'q',
|
||||
description: 'suppress warning messages',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvAddCmd,
|
||||
);
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const { exitWithExpectedError } = await import('../../utils/patterns');
|
||||
|
||||
const cmd = this;
|
||||
|
||||
await Bluebird.try(async function() {
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.name];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(
|
||||
`Environment value not found for variable: ${params.name}`,
|
||||
);
|
||||
} else if (!options.quiet) {
|
||||
cmd.warn(
|
||||
`Using ${params.name}=${params.value} from CLI process environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reservedPrefixes = await getReservedPrefixes();
|
||||
const isConfigVar = _.some(reservedPrefixes, prefix =>
|
||||
_.startsWith(params.name, prefix),
|
||||
);
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application[
|
||||
isConfigVar ? 'configVar' : 'envVar'
|
||||
].set(options.application, params.name, params.value);
|
||||
} else if (options.device) {
|
||||
return balena.models.device[isConfigVar ? 'configVar' : 'envVar'].set(
|
||||
options.device,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} else {
|
||||
exitWithExpectedError('You must specify an application or device');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function getReservedPrefixes(): Promise<string[]> {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const settings = await balena.settings.getAll();
|
||||
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
});
|
||||
|
||||
return response.body.reservedNamespaces;
|
||||
}
|
79
lib/actions-oclif/version.ts
Normal file
79
lib/actions-oclif/version.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface FlagsDef {
|
||||
all?: boolean;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export default class VersionCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
If you intend to parse the output, please use the -j option for
|
||||
JSON output, as its format is more stable.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena version',
|
||||
'$ balena version -a',
|
||||
'$ balena version -j',
|
||||
];
|
||||
|
||||
public static usage = 'version';
|
||||
|
||||
public static flags = {
|
||||
all: flags.boolean({
|
||||
char: 'a',
|
||||
default: false,
|
||||
description:
|
||||
'include version information for additional components (Node.js)',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
default: false,
|
||||
description:
|
||||
'output version information in JSON format for programmatic use',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(VersionCmd);
|
||||
const versions = {
|
||||
'balena-cli': (await import('../../package.json')).version,
|
||||
'Node.js':
|
||||
process.version && process.version.startsWith('v')
|
||||
? process.version.slice(1)
|
||||
: process.version,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(versions, null, 4));
|
||||
} else {
|
||||
if (options.all) {
|
||||
console.log(`balena-cli version "${versions['balena-cli']}"`);
|
||||
console.log(`Node.js version "${versions['Node.js']}"`);
|
||||
} else {
|
||||
// backwards compatibility
|
||||
console.log(versions['balena-cli']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
lib/actions/api-key.ts
Normal file
36
lib/actions/api-key.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const generate: CommandDefinition<{
|
||||
name: string;
|
||||
}> = {
|
||||
signature: 'api-key generate <name>',
|
||||
description: 'Generate a new API key with the given name',
|
||||
help: stripIndent`
|
||||
This command generates a new API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena api-key generate "Jenkins Key"
|
||||
`,
|
||||
async action(params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
|
||||
balena.models.apiKey
|
||||
.create(params.name)
|
||||
.then(key => {
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
})
|
||||
.finally(done);
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -20,44 +20,48 @@ exports.create =
|
||||
signature: 'app create <name>'
|
||||
description: 'create an application'
|
||||
help: '''
|
||||
Use this command to create a new resin.io application.
|
||||
Use this command to create a new balena application.
|
||||
|
||||
You can specify the application device type with the `--type` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
|
||||
You can see a list of supported device types with
|
||||
|
||||
$ resin devices supported
|
||||
$ balena devices supported
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app create MyApp
|
||||
$ resin app create MyApp --type raspberry-pi
|
||||
$ balena app create MyApp
|
||||
$ balena app create MyApp --type raspberry-pi
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
parameter: 'type'
|
||||
description: 'application device type (Check available types with `resin devices supported`)'
|
||||
description: 'application device type (Check available types with `balena devices supported`)'
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
# Validate the the application name is available
|
||||
# before asking the device type.
|
||||
# https://github.com/resin-io/resin-cli/issues/30
|
||||
resin.models.application.has(params.name).then (hasApplication) ->
|
||||
# https://github.com/balena-io/balena-cli/issues/30
|
||||
balena.models.application.has(params.name).then (hasApplication) ->
|
||||
if hasApplication
|
||||
throw new Error('You already have an application with that name!')
|
||||
patterns.exitWithExpectedError('You already have an application with that name!')
|
||||
|
||||
.then ->
|
||||
return options.type or patterns.selectDeviceType()
|
||||
.then (deviceType) ->
|
||||
return resin.models.application.create(params.name, deviceType)
|
||||
return balena.models.application.create({
|
||||
name: params.name
|
||||
deviceType
|
||||
})
|
||||
.then (application) ->
|
||||
console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})")
|
||||
.nodeify(done)
|
||||
@ -69,25 +73,37 @@ exports.list =
|
||||
Use this command to list all your applications.
|
||||
|
||||
Notice this command only shows the most important bits of information for each app.
|
||||
If you want detailed information, use resin app <name> instead.
|
||||
If you want detailed information, use balena app <name> instead.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin apps
|
||||
$ balena apps
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.getAll().then (applications) ->
|
||||
balena.models.application.getAll
|
||||
$select: [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
]
|
||||
$expand: owns__device: $select: 'is_online'
|
||||
.then (applications) ->
|
||||
applications.forEach (application) ->
|
||||
application.device_count = _.size(application.owns__device)
|
||||
application.online_devices = _.filter(application.owns__device, (d) -> d.is_online == true).length
|
||||
|
||||
console.log visuals.table.horizontal applications, [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
'online_devices'
|
||||
'devices_length'
|
||||
'device_count'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
@ -99,15 +115,15 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app MyApp
|
||||
$ balena app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.get(params.name).then (application) ->
|
||||
balena.models.application.get(params.name).then (application) ->
|
||||
console.log visuals.table.vertical application, [
|
||||
"$#{application.app_name}$"
|
||||
'id'
|
||||
@ -125,33 +141,33 @@ exports.restart =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app restart MyApp
|
||||
$ balena app restart MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.application.restart(params.name).nodeify(done)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.application.restart(params.name).nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'app rm <name>'
|
||||
description: 'remove an application'
|
||||
help: '''
|
||||
Use this command to remove a resin.io application.
|
||||
Use this command to remove a balena application.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app rm MyApp
|
||||
$ resin app rm MyApp --yes
|
||||
$ balena app rm MyApp
|
||||
$ balena app rm MyApp --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the application?').then ->
|
||||
resin.models.application.remove(params.name)
|
||||
balena.models.application.remove(params.name)
|
||||
.nodeify(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,9 +16,9 @@ limitations under the License.
|
||||
|
||||
exports.login =
|
||||
signature: 'login'
|
||||
description: 'login to resin.io'
|
||||
description: 'login to balena'
|
||||
help: '''
|
||||
Use this command to login to your resin.io account.
|
||||
Use this command to login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
@ -27,20 +27,20 @@ exports.login =
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using the authentication token from the preferences page.
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin login
|
||||
$ resin login --web
|
||||
$ resin login --token "..."
|
||||
$ resin login --credentials
|
||||
$ resin login --credentials --email johndoe@gmail.com --password secret
|
||||
$ balena login
|
||||
$ balena login --web
|
||||
$ balena login --token "..."
|
||||
$ balena login --credentials
|
||||
$ balena login --credentials --email johndoe@gmail.com --password secret
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'auth token'
|
||||
description: 'session token or API key'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
@ -57,23 +57,23 @@ exports.login =
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
]
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
@ -84,10 +84,15 @@ exports.login =
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Token (from the preferences page)'
|
||||
message: 'Session token or API key from the preferences page'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(resin.auth.loginWithToken)
|
||||
.then(balena.auth.loginWithToken)
|
||||
.tap ->
|
||||
balena.auth.whoami()
|
||||
.then (username) ->
|
||||
if !username
|
||||
patterns.exitWithExpectedError('Token authentication failed')
|
||||
else if options.credentials
|
||||
return patterns.authenticate(options)
|
||||
else if options.web
|
||||
@ -97,24 +102,25 @@ exports.login =
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
return capitanoRunAsync('signup')
|
||||
signupUrl = 'https://dashboard.balena-cloud.com/signup'
|
||||
require('opn')(signupUrl, { wait: false })
|
||||
patterns.exitWithExpectedError("Please sign up at #{signupUrl}")
|
||||
|
||||
options[loginType] = true
|
||||
return login(options)
|
||||
|
||||
resin.settings.get('resinUrl').then (resinUrl) ->
|
||||
console.log(messages.resinAsciiArt)
|
||||
console.log("\nLogging in to #{resinUrl}")
|
||||
balena.settings.get('balenaUrl').then (balenaUrl) ->
|
||||
console.log(messages.balenaAsciiArt)
|
||||
console.log("\nLogging in to #{balenaUrl}")
|
||||
return login(options)
|
||||
.then(resin.auth.whoami)
|
||||
.then(balena.auth.whoami)
|
||||
.tap (username) ->
|
||||
console.info("Successfully logged in as: #{username}")
|
||||
console.info """
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ resin help
|
||||
$ balena help
|
||||
|
||||
#{messages.reachingOut}
|
||||
"""
|
||||
@ -122,59 +128,17 @@ exports.login =
|
||||
|
||||
exports.logout =
|
||||
signature: 'logout'
|
||||
description: 'logout from resin.io'
|
||||
description: 'logout from balena'
|
||||
help: '''
|
||||
Use this command to logout from your resin.io account.o
|
||||
Use this command to logout from your balena account.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logout
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.auth.logout().nodeify(done)
|
||||
|
||||
exports.signup =
|
||||
signature: 'signup'
|
||||
description: 'signup to resin.io'
|
||||
help: '''
|
||||
Use this command to signup for a resin.io account.
|
||||
|
||||
If signup is successful, you'll be logged in to your new user automatically.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin signup
|
||||
Email: johndoe@acme.com
|
||||
Password: ***********
|
||||
|
||||
$ resin whoami
|
||||
johndoe
|
||||
$ balena logout
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
form = require('resin-cli-form')
|
||||
validation = require('../utils/validation')
|
||||
|
||||
resin.settings.get('resinUrl').then (resinUrl) ->
|
||||
console.log("\nRegistering to #{resinUrl}")
|
||||
|
||||
form.run [
|
||||
message: 'Email:'
|
||||
name: 'email'
|
||||
type: 'input'
|
||||
validate: validation.validateEmail
|
||||
,
|
||||
message: 'Password:'
|
||||
name: 'password'
|
||||
type: 'password',
|
||||
validate: validation.validatePassword
|
||||
]
|
||||
|
||||
.then(resin.auth.register)
|
||||
.then(resin.auth.loginWithToken)
|
||||
.nodeify(done)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.auth.logout().nodeify(done)
|
||||
|
||||
exports.whoami =
|
||||
signature: 'whoami'
|
||||
@ -184,18 +148,18 @@ exports.whoami =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin whoami
|
||||
$ balena whoami
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
email: resin.auth.getEmail()
|
||||
url: resin.settings.get('resinUrl')
|
||||
username: balena.auth.whoami()
|
||||
email: balena.auth.getEmail()
|
||||
url: balena.settings.get('balenaUrl')
|
||||
.then (results) ->
|
||||
console.log visuals.table.vertical results, [
|
||||
'$account information$'
|
||||
|
@ -2,42 +2,83 @@
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
getBundleInfo = Promise.method (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
if options.application?
|
||||
# An application was provided
|
||||
return helpers.getAppInfo(options.application)
|
||||
.then (app) ->
|
||||
return [app.arch, app.device_type]
|
||||
else if options.arch? and options.deviceType?
|
||||
return [options.arch, options.deviceType]
|
||||
else
|
||||
# No information, cannot do resolution
|
||||
return undefined
|
||||
app: the app this build is for (optional)
|
||||
arch: the architecture to build for
|
||||
deviceType: the device type to build for
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
buildProject = (docker, logger, composeOpts, opts) ->
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
undefined # image: name of pre-built image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.then (project) ->
|
||||
appType = opts.app?.application_type?[0]
|
||||
if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.'
|
||||
)
|
||||
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
project.composition
|
||||
opts.arch
|
||||
opts.deviceType
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then ->
|
||||
logger.logSuccess('Build succeeded!')
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Build failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'build [source]'
|
||||
description: 'Build a container locally'
|
||||
permission: 'user'
|
||||
help: '''
|
||||
Use this command to build a container with a provided docker daemon.
|
||||
description: 'Build a single image or a multicontainer project locally'
|
||||
primary: true
|
||||
help: """
|
||||
Use this command to build an image or a complete multicontainer project with
|
||||
the provided docker daemon in your development machine or balena device.
|
||||
(See also the `balena push` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture
|
||||
pair to use the resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
You must provide either an application or a device-type/architecture pair to use
|
||||
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file. If it is found,
|
||||
this command will build each service defined in the compose file. If a compose
|
||||
file isn't found, the command will look for a Dockerfile[.template] file (or
|
||||
alternative Dockerfile specified with the `-f` option), and if yet that isn't
|
||||
found, it will try to generate one.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin build
|
||||
$ resin build ./source/
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf
|
||||
$ resin build --application MyApp ./source/
|
||||
$ resin build --docker '/var/run/docker.sock'
|
||||
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
'''
|
||||
options: dockerUtils.appendOptions [
|
||||
$ balena build
|
||||
$ balena build ./source/
|
||||
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||||
$ balena build --application MyApp ./source/
|
||||
$ balena build --docker '/var/run/docker.sock'
|
||||
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
"""
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
@ -53,12 +94,63 @@ module.exports =
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target resin.io application this build is for'
|
||||
description: 'The target balena application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Logger = require('../utils/logger')
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, new Logger())
|
||||
.asCallback(done)
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
|
||||
sdk = (require('balena-sdk')).fromSharedOptions()
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = 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(sdk, 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,100 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
exports.yes =
|
||||
signature: 'yes'
|
||||
description: 'confirm non interactively'
|
||||
boolean: true
|
||||
alias: 'y'
|
||||
|
||||
exports.optionalApplication =
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'application name'
|
||||
alias: [ 'a', 'app' ]
|
||||
|
||||
exports.application = _.defaults
|
||||
required: 'You have to specify an application'
|
||||
, exports.optionalApplication
|
||||
|
||||
exports.optionalDevice =
|
||||
signature: 'device'
|
||||
parameter: 'device'
|
||||
description: 'device uuid'
|
||||
alias: 'd'
|
||||
|
||||
exports.optionalDeviceApiKey =
|
||||
signature: 'deviceApiKey'
|
||||
description: 'custom device key - note that this is only supported on ResinOS 2.0.3+'
|
||||
parameter: 'device-api-key'
|
||||
alias: 'k'
|
||||
|
||||
exports.booleanDevice =
|
||||
signature: 'device'
|
||||
description: 'device'
|
||||
boolean: true
|
||||
alias: 'd'
|
||||
|
||||
exports.osVersion =
|
||||
signature: 'version'
|
||||
description: """
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)
|
||||
"""
|
||||
parameter: 'version'
|
||||
|
||||
exports.network =
|
||||
signature: 'network'
|
||||
parameter: 'network'
|
||||
description: 'network type'
|
||||
alias: 'n'
|
||||
|
||||
exports.wifiSsid =
|
||||
signature: 'ssid'
|
||||
parameter: 'ssid'
|
||||
description: 'wifi ssid, if network is wifi'
|
||||
alias: 's'
|
||||
|
||||
exports.wifiKey =
|
||||
signature: 'key'
|
||||
parameter: 'key'
|
||||
description: 'wifi key, if network is wifi'
|
||||
alias: 'k'
|
||||
|
||||
exports.forceUpdateLock =
|
||||
signature: 'force'
|
||||
description: 'force action if the update lock is set'
|
||||
boolean: true
|
||||
alias: 'f'
|
||||
|
||||
exports.drive =
|
||||
signature: 'drive'
|
||||
description: 'the drive to write the image to, like `/dev/sdb` or `/dev/mmcblk0`.
|
||||
Careful with this as you can erase your hard drive.
|
||||
Check `resin util available-drives` for available options.'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
|
||||
exports.advancedConfig =
|
||||
signature: 'advanced'
|
||||
description: 'show advanced configuration options'
|
||||
boolean: true
|
||||
alias: 'v'
|
157
lib/actions/command-options.ts
Normal file
157
lib/actions/command-options.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import _ = require('lodash');
|
||||
|
||||
export const yes = {
|
||||
signature: 'yes',
|
||||
description: 'confirm non interactively',
|
||||
boolean: true,
|
||||
alias: 'y',
|
||||
};
|
||||
|
||||
export interface YesOption {
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
export const optionalApplication = {
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'application name',
|
||||
alias: ['a', 'app'],
|
||||
};
|
||||
|
||||
export const application = _.defaults(
|
||||
{ required: 'You have to specify an application' },
|
||||
optionalApplication,
|
||||
);
|
||||
|
||||
export const optionalRelease = {
|
||||
signature: 'release',
|
||||
parameter: 'release',
|
||||
description: 'release id',
|
||||
alias: 'r',
|
||||
};
|
||||
|
||||
export const optionalDevice = {
|
||||
signature: 'device',
|
||||
parameter: 'device',
|
||||
description: 'device uuid',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const optionalDeviceApiKey = {
|
||||
signature: 'deviceApiKey',
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
parameter: 'device-api-key',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const optionalDeviceType = {
|
||||
signature: 'deviceType',
|
||||
description: 'device type slug',
|
||||
parameter: 'device-type',
|
||||
};
|
||||
|
||||
export const optionalOsVersion = {
|
||||
signature: 'version',
|
||||
description: 'a balenaOS version',
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export type OptionalOsVersionOption = Partial<OsVersionOption>;
|
||||
|
||||
export const osVersion = _.defaults(
|
||||
{
|
||||
required: 'You have to specify an exact os version',
|
||||
},
|
||||
exports.optionalOsVersion,
|
||||
);
|
||||
|
||||
export interface OsVersionOption {
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export const booleanDevice = {
|
||||
signature: 'device',
|
||||
description: 'device',
|
||||
boolean: true,
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const osVersionOrSemver = {
|
||||
signature: 'version',
|
||||
description: `\
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
or 'default' (excludes pre-releases if at least one stable version is available),
|
||||
or 'recommended' (excludes pre-releases, will fail if only pre-release versions are available),
|
||||
or 'menu' (will show the interactive menu)\
|
||||
`,
|
||||
parameter: 'version',
|
||||
};
|
||||
|
||||
export const network = {
|
||||
signature: 'network',
|
||||
parameter: 'network',
|
||||
description: 'network type',
|
||||
alias: 'n',
|
||||
};
|
||||
|
||||
export const wifiSsid = {
|
||||
signature: 'ssid',
|
||||
parameter: 'ssid',
|
||||
description: 'wifi ssid, if network is wifi',
|
||||
alias: 's',
|
||||
};
|
||||
|
||||
export const wifiKey = {
|
||||
signature: 'key',
|
||||
parameter: 'key',
|
||||
description: 'wifi key, if network is wifi',
|
||||
alias: 'k',
|
||||
};
|
||||
|
||||
export const forceUpdateLock = {
|
||||
signature: 'force',
|
||||
description: 'force action if the update lock is set',
|
||||
boolean: true,
|
||||
alias: 'f',
|
||||
};
|
||||
|
||||
export const drive = {
|
||||
signature: 'drive',
|
||||
description: `the drive to write the image to, like \`/dev/sdb\` or \`/dev/mmcblk0\`. \
|
||||
Careful with this as you can erase your hard drive. \
|
||||
Check \`balena util available-drives\` for available options.`,
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
};
|
||||
|
||||
export const advancedConfig = {
|
||||
signature: 'advanced',
|
||||
description: 'show advanced configuration options',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
};
|
||||
|
||||
export const hostOSAccess = {
|
||||
signature: 'host',
|
||||
boolean: true,
|
||||
description: 'access host OS (for devices with balenaOS >= 2.0.0+rev1)',
|
||||
alias: 's',
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2018 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,6 +15,7 @@ limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.read =
|
||||
signature: 'config read'
|
||||
@ -24,13 +25,13 @@ exports.read =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config read --type raspberry-pi
|
||||
$ resin config read --type raspberry-pi --drive /dev/disk2
|
||||
$ balena config read --type raspberry-pi
|
||||
$ balena config read --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -46,7 +47,7 @@ exports.read =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
prettyjson = require('prettyjson')
|
||||
@ -68,14 +69,14 @@ exports.write =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config write --type raspberry-pi username johndoe
|
||||
$ resin config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ resin config write --type raspberry-pi files.network/settings "..."
|
||||
$ balena config write --type raspberry-pi username johndoe
|
||||
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ balena config write --type raspberry-pi files.network/settings "..."
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -92,7 +93,7 @@ exports.write =
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
@ -116,17 +117,18 @@ exports.inject =
|
||||
signature: 'config inject <file>'
|
||||
description: 'inject a device configuration file'
|
||||
help: '''
|
||||
Use this command to inject a config.json file to the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
Use this command to inject a config.json file to the mounted filesystem
|
||||
(e.g. SD card or mounted balenaOS image) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config inject my/config.json --type raspberry-pi
|
||||
$ resin config inject my/config.json --type raspberry-pi --drive /dev/disk2
|
||||
$ balena config inject my/config.json --type raspberry-pi
|
||||
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -142,7 +144,7 @@ exports.inject =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
@ -165,14 +167,14 @@ exports.reconfigure =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config reconfigure --type raspberry-pi
|
||||
$ resin config reconfigure --type raspberry-pi --advanced
|
||||
$ resin config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
$ balena config reconfigure --type raspberry-pi
|
||||
$ balena config reconfigure --type raspberry-pi --advanced
|
||||
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -194,9 +196,9 @@ exports.reconfigure =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
config = require('balena-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
@ -207,10 +209,10 @@ exports.reconfigure =
|
||||
.tap ->
|
||||
umountAsync(drive)
|
||||
.then (uuid) ->
|
||||
configureCommand = "os configure #{drive} #{uuid}"
|
||||
configureCommand = "os configure #{drive} --device #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return capitanoRunAsync(configureCommand)
|
||||
return runCommand(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
@ -221,23 +223,38 @@ exports.generate =
|
||||
help: '''
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config generate --device 7cf02a6
|
||||
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --output config.json
|
||||
$ resin config generate --app MyApp
|
||||
$ resin config generate --app MyApp --output config.json
|
||||
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7
|
||||
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
|
||||
$ balena config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
'''
|
||||
options: [
|
||||
commandOptions.osVersion
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
{
|
||||
signature: 'generate-device-api-key'
|
||||
description: 'generate a fresh device key for the device'
|
||||
boolean: true
|
||||
}
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output'
|
||||
@ -268,42 +285,71 @@ exports.generate =
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
deviceConfig = require('resin-device-config')
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
helpers = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
throw new Error '''
|
||||
exitWithExpectedError '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ resin help config generate
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
if !options.application and options.deviceType
|
||||
exitWithExpectedError '''
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
'''
|
||||
|
||||
Promise.try ->
|
||||
if options.device?
|
||||
return resin.models.device.get(options.device)
|
||||
return resin.models.application.get(options.application)
|
||||
return balena.models.device.get(options.device)
|
||||
return balena.models.application.get(options.application)
|
||||
.then (resource) ->
|
||||
resin.models.device.getManifestBySlug(resource.device_type)
|
||||
.get('options')
|
||||
deviceType = options.deviceType || resource.device_type
|
||||
manifestPromise = balena.models.device.getManifestBySlug(deviceType)
|
||||
|
||||
if options.application && options.deviceType
|
||||
app = resource
|
||||
appManifestPromise = balena.models.device.getManifestBySlug(app.device_type)
|
||||
manifestPromise = manifestPromise.tap (paramDeviceType) ->
|
||||
appManifestPromise.then (appDeviceType) ->
|
||||
if not helpers.areDeviceTypesCompatible(appDeviceType, paramDeviceType)
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
"Device type #{options.deviceType} is incompatible with application #{options.application}"
|
||||
)
|
||||
|
||||
manifestPromise.get('options')
|
||||
.then (formOptions) ->
|
||||
# Pass params as an override: if there is any param with exactly the same name as a
|
||||
# required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, override: options)
|
||||
.then (answers) ->
|
||||
answers.version = options.version
|
||||
|
||||
if resource.uuid?
|
||||
generateDeviceConfig(resource, options.deviceApiKey, answers)
|
||||
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
|
||||
else
|
||||
answers.deviceType = deviceType
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
deviceConfig.validate(config)
|
||||
if options.output?
|
||||
return writeFileAsync(options.output, JSON.stringify(config))
|
||||
|
||||
|
@ -1,145 +1,176 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
compose = require('../utils/compose')
|
||||
{ registrySecretsHelp } = require('../utils/messages')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app })
|
||||
"https://builder.#{baseUrl}/v1/push?#{args}"
|
||||
###
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app, buildId })
|
||||
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
dockerfilePath: name of an alternative Dockerfile; optional
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
###
|
||||
deployProject = (docker, logger, composeOpts, opts) ->
|
||||
_ = require('lodash')
|
||||
doodles = require('resin-doodles')
|
||||
sdk = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
formatImageName = (image) ->
|
||||
image.split('/').pop()
|
||||
compose.loadProject(
|
||||
logger
|
||||
composeOpts.projectPath
|
||||
composeOpts.projectName
|
||||
opts.image
|
||||
composeOpts.dockerfilePath # ok if undefined
|
||||
)
|
||||
.then (project) ->
|
||||
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
|
||||
throw new Error('Target application does not support multiple containers. Aborting!')
|
||||
|
||||
parseInput = Promise.method (params, options) ->
|
||||
if not params.appName?
|
||||
throw new Error('Need an application to deploy to!')
|
||||
appName = params.appName
|
||||
image = undefined
|
||||
if params.image?
|
||||
if options.build or options.source?
|
||||
throw new Error('Build and source parameters are not applicable when specifying an image')
|
||||
options.build = false
|
||||
image = params.image
|
||||
else if options.build
|
||||
source = options.source || '.'
|
||||
else
|
||||
throw new Error('Need either an image or a build flag!')
|
||||
# find which services use images that already exist locally
|
||||
Promise.map project.descriptors, (d) ->
|
||||
# unconditionally build (or pull) if explicitly requested
|
||||
return d if opts.shouldPerformBuild
|
||||
docker.getImage(d.image.tag ? d.image).inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn()
|
||||
.filter (d) -> !!d
|
||||
.then (servicesToSkip) ->
|
||||
# multibuild takes in a composition and always attempts to
|
||||
# build or pull all services. we workaround that here by
|
||||
# passing a modified composition.
|
||||
compositionToBuild = _.cloneDeep(project.composition)
|
||||
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
|
||||
if _.size(compositionToBuild.services) is 0
|
||||
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
|
||||
return {}
|
||||
compose.buildProject(
|
||||
docker
|
||||
logger
|
||||
project.path
|
||||
project.name
|
||||
compositionToBuild
|
||||
opts.app.arch
|
||||
opts.app.device_type
|
||||
opts.buildEmulated
|
||||
opts.buildOpts
|
||||
composeOpts.inlineLogs
|
||||
)
|
||||
.then (builtImages) ->
|
||||
_.keyBy(builtImages, 'serviceName')
|
||||
.then (builtImages) ->
|
||||
project.descriptors.map (d) ->
|
||||
builtImages[d.serviceName] ? {
|
||||
serviceName: d.serviceName,
|
||||
name: d.image.tag ? d.image
|
||||
logs: 'Build skipped; image for service already exists.'
|
||||
props: {}
|
||||
}
|
||||
.then (images) ->
|
||||
if opts.app.application_type?[0]?.is_legacy
|
||||
chalk = require('chalk')
|
||||
legacyDeploy = require('../utils/deploy-legacy')
|
||||
|
||||
return [appName, options.build, source, image]
|
||||
msg = chalk.yellow('Target application requires legacy deploy method.')
|
||||
logger.logWarn(msg)
|
||||
|
||||
showPushProgress = (message) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
progressBar = new visuals.Progress(message)
|
||||
progressBar.update({ percentage: 0 })
|
||||
return progressBar
|
||||
|
||||
getBundleInfo = (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getAppInfo(options.appName)
|
||||
.then (app) ->
|
||||
[app.arch, app.device_type]
|
||||
|
||||
performUpload = (imageStream, token, username, url, appName, logger) ->
|
||||
request = require('request')
|
||||
progressStream = require('progress-stream')
|
||||
zlib = require('zlib')
|
||||
|
||||
# Need to strip off the newline
|
||||
progressMessage = logger.formatMessage('info', 'Deploying').slice(0, -1)
|
||||
progressBar = showPushProgress(progressMessage)
|
||||
streamWithProgress = imageStream.pipe progressStream
|
||||
time: 500,
|
||||
length: imageStream.length
|
||||
, ({ percentage, eta }) ->
|
||||
progressBar.update
|
||||
percentage: Math.min(percentage, 100)
|
||||
eta: eta
|
||||
|
||||
uploadRequest = request.post
|
||||
url: getBuilderPushEndpoint(url, username, appName)
|
||||
headers:
|
||||
'Content-Encoding': 'gzip'
|
||||
auth:
|
||||
bearer: token
|
||||
body: streamWithProgress.pipe(zlib.createGzip({
|
||||
level: 6
|
||||
}))
|
||||
|
||||
uploadToPromise(uploadRequest, logger)
|
||||
|
||||
uploadLogs = (logs, token, url, buildId, username, appName) ->
|
||||
request = require('request')
|
||||
request.post
|
||||
json: true
|
||||
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
|
||||
auth:
|
||||
bearer: token
|
||||
body: Buffer.from(logs)
|
||||
|
||||
uploadToPromise = (uploadRequest, logger) ->
|
||||
new Promise (resolve, reject) ->
|
||||
|
||||
handleMessage = (data) ->
|
||||
data = data.toString()
|
||||
logger.logDebug("Received data: #{data}")
|
||||
|
||||
try
|
||||
obj = JSON.parse(data)
|
||||
catch e
|
||||
logger.logError('Error parsing reply from remote side')
|
||||
reject(e)
|
||||
return
|
||||
|
||||
if obj.type?
|
||||
switch obj.type
|
||||
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
||||
when 'success' then resolve(obj)
|
||||
when 'status' then logger.logInfo("Remote: #{obj.message}")
|
||||
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
else
|
||||
reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
|
||||
uploadRequest
|
||||
.on('error', reject)
|
||||
.on('data', handleMessage)
|
||||
return Promise.join(
|
||||
docker
|
||||
logger
|
||||
sdk.auth.getToken()
|
||||
sdk.auth.whoami()
|
||||
sdk.settings.get('balenaUrl')
|
||||
{
|
||||
# opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName
|
||||
imageName: images[0].name
|
||||
buildLogs: images[0].logs
|
||||
shouldUploadLogs: opts.shouldUploadLogs
|
||||
}
|
||||
legacyDeploy
|
||||
)
|
||||
.then (releaseId) ->
|
||||
sdk.models.release.get(releaseId, $select: [ 'commit' ])
|
||||
Promise.join(
|
||||
sdk.auth.getUserId()
|
||||
sdk.auth.getToken()
|
||||
sdk.settings.get('apiUrl')
|
||||
(userId, auth, apiEndpoint) ->
|
||||
compose.deployProject(
|
||||
docker
|
||||
logger
|
||||
project.composition
|
||||
images
|
||||
opts.app.id
|
||||
userId
|
||||
"Bearer #{auth}"
|
||||
apiEndpoint
|
||||
!opts.shouldUploadLogs
|
||||
)
|
||||
)
|
||||
.then (release) ->
|
||||
logger.logSuccess('Deploy succeeded!')
|
||||
logger.logSuccess("Release: #{release.commit}")
|
||||
console.log()
|
||||
console.log(doodles.getDoodle()) # Show charlie
|
||||
console.log()
|
||||
.tapCatch (e) ->
|
||||
logger.logError('Deploy failed')
|
||||
|
||||
module.exports =
|
||||
signature: 'deploy <appName> [image]'
|
||||
description: 'Deploy an image to a resin.io application'
|
||||
help: '''
|
||||
Use this command to deploy an image to an application, optionally building it first.
|
||||
|
||||
description: 'Deploy a single image or a multicontainer project to a balena application'
|
||||
help: """
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`resin deploy <appOwnerUsername>/<appName>`.
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the `balena push` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
|
||||
Note: If building with this command, all options supported by `resin build`
|
||||
are also supported with this command.
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml file. If one is
|
||||
found, this command will deploy each service defined in the compose file,
|
||||
building it first if an image for it doesn't exist. If a compose file isn't
|
||||
found, the command will look for a Dockerfile[.template] file (or alternative
|
||||
Dockerfile specified with the `-f` option), and if yet that isn't found, it
|
||||
will try to generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`balena deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
When --build is used, all options supported by `balena build` are also supported
|
||||
by this command.
|
||||
|
||||
#{registrySecretsHelp}
|
||||
|
||||
Examples:
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
'''
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage
|
||||
"""
|
||||
permission: 'user'
|
||||
options: dockerUtils.appendOptions [
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Build image then deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
primary: true
|
||||
options: dockerUtils.appendOptions compose.appendOptions [
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'The source directory to use when building the image'
|
||||
description: 'Specify an alternate source directory; default is the working directory'
|
||||
alias: 's'
|
||||
},
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Force a rebuild before deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'nologupload'
|
||||
description: "Don't upload build logs to the dashboard with image (if building)"
|
||||
@ -147,83 +178,60 @@ module.exports =
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
# compositions with many services trigger misleading warnings
|
||||
require('events').defaultMaxListeners = 1000
|
||||
sdk = (require('balena-sdk')).fromSharedOptions()
|
||||
{ validateComposeOptions } = require('../utils/compose_ts')
|
||||
helpers = require('../utils/helpers')
|
||||
Logger = require('../utils/logger')
|
||||
|
||||
logger = new Logger()
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
logger.logDebug('Parsing input...')
|
||||
|
||||
logs = ''
|
||||
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
|
||||
|
||||
upload = (token, username, url) ->
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
# Check input parameters
|
||||
parseInput(params, options)
|
||||
.then ([appName, build, source, imageName]) ->
|
||||
tmpNameAsync()
|
||||
.then (bufferFile) ->
|
||||
# look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application
|
||||
delete options.application
|
||||
|
||||
# Setup the build args for how the build routine expects them
|
||||
options = _.assign({}, options, { appName })
|
||||
params = _.assign({}, params, { source })
|
||||
validateComposeOptions(sdk, options)
|
||||
|
||||
Promise.try ->
|
||||
if build
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, logger)
|
||||
else
|
||||
{ image: imageName, log: '' }
|
||||
.then ({ image: imageName, log: buildLogs }) ->
|
||||
logger.logInfo('Initializing deploy...')
|
||||
if not appName?
|
||||
throw new Error('Please specify the name of the application to deploy')
|
||||
|
||||
logs = buildLogs
|
||||
Promise.all [
|
||||
dockerUtils.bufferImage(docker, imageName, bufferFile)
|
||||
token
|
||||
username
|
||||
url
|
||||
params.appName
|
||||
logger
|
||||
]
|
||||
.spread(performUpload)
|
||||
.finally ->
|
||||
# If the file was never written to (for instance because an error
|
||||
# has occured before any data was written) this call will throw an
|
||||
# ugly error, just suppress it
|
||||
Promise.try ->
|
||||
require('mz/fs').unlink(bufferFile)
|
||||
.catch(_.noop)
|
||||
.tap ({ image: imageName, buildId }) ->
|
||||
logger.logSuccess("Successfully deployed image: #{formatImageName(imageName)}")
|
||||
return buildId
|
||||
.then ({ image: imageName, buildId }) ->
|
||||
if logs is '' or options.nologupload?
|
||||
return ''
|
||||
if image? and options.build
|
||||
throw new Error('Build option is not applicable when specifying an image')
|
||||
|
||||
logger.logInfo('Uploading logs to dashboard...')
|
||||
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 ]
|
||||
|
||||
Promise.join(
|
||||
logs
|
||||
token
|
||||
url
|
||||
buildId
|
||||
username
|
||||
params.appName
|
||||
uploadLogs
|
||||
)
|
||||
.return('Successfully uploaded logs')
|
||||
.then (msg) ->
|
||||
logger.logSuccess(msg) if msg isnt ''
|
||||
.asCallback(done)
|
||||
|
||||
Promise.join(
|
||||
resin.auth.getToken()
|
||||
resin.auth.whoami()
|
||||
resin.settings.get('resinUrl')
|
||||
upload
|
||||
)
|
||||
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
|
||||
Promise.join(
|
||||
dockerUtils.getDocker(options)
|
||||
dockerUtils.generateBuildOpts(options)
|
||||
compose.generateOpts(options)
|
||||
(docker, buildOpts, composeOpts) ->
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app
|
||||
appName # may be prefixed by 'owner/', unlike app.app_name
|
||||
image
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated: !!options.emulated
|
||||
buildOpts
|
||||
})
|
||||
)
|
||||
.asCallback(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,6 +16,11 @@ limitations under the License.
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
expandForAppName = {
|
||||
$expand: belongs_to__application: $select: 'app_name'
|
||||
}
|
||||
|
||||
exports.list =
|
||||
signature: 'devices'
|
||||
@ -27,33 +32,35 @@ exports.list =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices
|
||||
$ resin devices --application MyApp
|
||||
$ resin devices --app MyApp
|
||||
$ resin devices -a MyApp
|
||||
$ balena devices
|
||||
$ balena devices --application MyApp
|
||||
$ balena devices --app MyApp
|
||||
$ balena devices -a MyApp
|
||||
'''
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.device.getAllByApplication(options.application)
|
||||
return resin.models.device.getAll()
|
||||
return balena.models.device.getAllByApplication(options.application, expandForAppName)
|
||||
return balena.models.device.getAll(expandForAppName)
|
||||
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.uuid = device.uuid.slice(0, 7)
|
||||
return device
|
||||
|
||||
console.log visuals.table.horizontal devices, [
|
||||
'id'
|
||||
'uuid'
|
||||
'name'
|
||||
'device_name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
@ -72,21 +79,25 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device 7cf02a6
|
||||
$ balena device 7cf02a6
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
|
||||
resin.models.device.getStatus(device).then (status) ->
|
||||
balena.models.device.get(params.uuid, expandForAppName)
|
||||
.then (device) ->
|
||||
balena.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid)
|
||||
device.application_name = device.belongs_to__application[0].app_name
|
||||
device.commit = device.is_on__commit
|
||||
|
||||
console.log visuals.table.vertical device, [
|
||||
"$#{device.name}$"
|
||||
"$#{device.device_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
@ -112,13 +123,13 @@ exports.supported =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices supported
|
||||
$ balena devices supported
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.config.getDeviceTypes().then (deviceTypes) ->
|
||||
balena.models.config.getDeviceTypes().then (deviceTypes) ->
|
||||
console.log visuals.table.horizontal deviceTypes, [
|
||||
'slug'
|
||||
'name'
|
||||
@ -131,13 +142,10 @@ exports.register =
|
||||
help: '''
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device register MyApp
|
||||
$ resin device register MyApp --uuid <uuid>
|
||||
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
|
||||
$ balena device register MyApp
|
||||
$ balena device register MyApp --uuid <uuid>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -147,23 +155,17 @@ exports.register =
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
commandOptions.optionalDeviceApiKey
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.join(
|
||||
resin.models.application.get(params.application)
|
||||
options.uuid ? resin.models.device.generateUniqueKey()
|
||||
options.deviceApiKey ? resin.models.device.generateUniqueKey()
|
||||
(application, uuid, deviceApiKey) ->
|
||||
balena.models.application.get(params.application)
|
||||
options.uuid ? balena.models.device.generateUniqueKey()
|
||||
(application, uuid) ->
|
||||
console.info("Registering to #{application.app_name}: #{uuid}")
|
||||
if not options.deviceApiKey?
|
||||
console.info("Using generated device api key: #{deviceApiKey}")
|
||||
else
|
||||
console.info('Using provided device api key')
|
||||
return resin.models.device.register(application.id, uuid, deviceApiKey)
|
||||
return balena.models.device.register(application.id, uuid)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
@ -172,24 +174,25 @@ exports.remove =
|
||||
signature: 'device rm <uuid>'
|
||||
description: 'remove a device'
|
||||
help: '''
|
||||
Use this command to remove a device from resin.io.
|
||||
Use this command to remove a device from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device rm 7cf02a6
|
||||
$ resin device rm 7cf02a6 --yes
|
||||
$ balena device rm 7cf02a6
|
||||
$ balena device rm 7cf02a6 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
|
||||
resin.models.device.remove(params.uuid)
|
||||
balena.models.device.remove(params.uuid)
|
||||
.nodeify(done)
|
||||
|
||||
exports.identify =
|
||||
@ -202,12 +205,13 @@ exports.identify =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device identify 23c73a1
|
||||
$ balena device identify 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.identify(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
signature: 'device reboot <uuid>'
|
||||
@ -217,13 +221,14 @@ exports.reboot =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device reboot 23c73a1
|
||||
$ balena device reboot 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.reboot(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.shutdown =
|
||||
signature: 'device shutdown <uuid>'
|
||||
@ -233,13 +238,14 @@ exports.shutdown =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device shutdown 23c73a1
|
||||
$ balena device shutdown 23c73a1
|
||||
'''
|
||||
options: [ commandOptions.forceUpdateLock ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.shutdown(params.uuid, options).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.shutdown(params.uuid, options).nodeify(done)
|
||||
|
||||
exports.enableDeviceUrl =
|
||||
signature: 'device public-url enable <uuid>'
|
||||
@ -249,12 +255,13 @@ exports.enableDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url enable 23c73a1
|
||||
$ balena device public-url enable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
signature: 'device public-url disable <uuid>'
|
||||
@ -264,12 +271,13 @@ exports.disableDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url disable 23c73a1
|
||||
$ balena device public-url disable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
signature: 'device public-url <uuid>'
|
||||
@ -279,12 +287,13 @@ exports.getDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url 23c73a1
|
||||
$ balena device public-url 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
|
||||
@ -296,18 +305,19 @@ exports.hasDeviceUrl =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url status 23c73a1
|
||||
$ balena device public-url status 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
console.log(hasDeviceUrl)
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'device rename <uuid> [newName]'
|
||||
description: 'rename a resin device'
|
||||
description: 'rename a balena device'
|
||||
help: '''
|
||||
Use this command to rename a device.
|
||||
|
||||
@ -315,13 +325,14 @@ exports.rename =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device rename 7cf02a6
|
||||
$ resin device rename 7cf02a6 MyPi
|
||||
$ balena device rename 7cf02a6
|
||||
$ balena device rename 7cf02a6 MyPi
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(params)
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
@ -331,7 +342,7 @@ exports.rename =
|
||||
message: 'How do you want to name this device?'
|
||||
type: 'input'
|
||||
|
||||
.then(_.partial(resin.models.device.rename, params.uuid))
|
||||
.then(_.partial(balena.models.device.rename, params.uuid))
|
||||
.nodeify(done)
|
||||
|
||||
exports.move =
|
||||
@ -344,30 +355,31 @@ exports.move =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device move 7cf02a6
|
||||
$ resin device move 7cf02a6 --application MyNewApp
|
||||
$ balena device move 7cf02a6
|
||||
$ balena device move 7cf02a6 --application MyNewApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
normalizeUuidProp(params)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
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.application_name isnt application.app_name
|
||||
device.belongs_to__application[0].app_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return resin.models.device.move(params.uuid, application)
|
||||
return balena.models.device.move(params.uuid, application)
|
||||
.then (application) ->
|
||||
console.info("#{params.uuid} was moved to #{application}")
|
||||
.nodeify(done)
|
||||
|
||||
exports.init =
|
||||
signature: 'device init'
|
||||
description: 'initialise a device with resinOS'
|
||||
description: 'initialise a device with balenaOS'
|
||||
help: '''
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
@ -376,71 +388,72 @@ exports.init =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device init
|
||||
$ resin device init --application MyApp
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.yes
|
||||
commandOptions.advancedConfig
|
||||
_.assign({}, commandOptions.osVersion, { signature: 'os-version', parameter: 'os-version' })
|
||||
_.assign({}, commandOptions.osVersionOrSemver, { signature: 'os-version', parameter: 'os-version' })
|
||||
commandOptions.drive
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin os build-config`'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
rimraf = Promise.promisify(require('rimraf'))
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
helpers = require('../utils/helpers')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
{ runCommand } = require('../utils/helpers')
|
||||
|
||||
Promise.try ->
|
||||
return options.application if options.application?
|
||||
return patterns.selectApplication()
|
||||
.then(resin.models.application.get)
|
||||
.then(balena.models.application.get)
|
||||
.then (application) ->
|
||||
|
||||
download = ->
|
||||
tmpNameAsync().then (tempPath) ->
|
||||
osVersion = options['os-version'] or 'default'
|
||||
capitanoRunAsync("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
capitanoRunAsync("device register #{application.app_name}")
|
||||
.then(resin.models.device.get)
|
||||
runCommand("device register #{application.app_name}")
|
||||
.then(balena.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' #{device.uuid}"
|
||||
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
|
||||
if options.config
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
capitanoRunAsync(configureCommand)
|
||||
runCommand(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
capitanoRunAsync(osInitCommand)
|
||||
runCommand(osInitCommand)
|
||||
# Make sure the device resource is removed if there is an
|
||||
# error when configuring or initializing a device image
|
||||
.catch (error) ->
|
||||
resin.models.device.remove(device.uuid).finally ->
|
||||
balena.models.device.remove(device.uuid).finally ->
|
||||
throw error
|
||||
.then (device) ->
|
||||
console.log('Done')
|
||||
return device.uuid
|
||||
|
||||
.nodeify(done)
|
||||
|
||||
exports.osUpdate = require('./device_ts').osUpdate
|
||||
|
115
lib/actions/device_ts.ts
Normal file
115
lib/actions/device_ts.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { Device } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
// tslint:disable-next-line:no-namespace
|
||||
namespace OsUpdate {
|
||||
export interface Args {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export type Options = commandOptions.OptionalOsVersionOption &
|
||||
commandOptions.YesOption;
|
||||
}
|
||||
|
||||
export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
|
||||
signature: 'device os-update <uuid>',
|
||||
description: 'Start a Host OS update for a device',
|
||||
help: stripIndent`
|
||||
Use this command to trigger a Host OS update for a device.
|
||||
|
||||
Notice this command will ask for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device os-update 23c73a1
|
||||
$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod
|
||||
`,
|
||||
options: [commandOptions.optionalOsVersion, commandOptions.yes],
|
||||
permission: 'user',
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = await import('balena-sdk');
|
||||
const _ = await import('lodash');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const patterns = await import('../utils/patterns');
|
||||
const form = await import('resin-cli-form');
|
||||
|
||||
return sdk.models.device
|
||||
.get(params.uuid, {
|
||||
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
|
||||
})
|
||||
.then(async ({ uuid, device_type, os_version, os_variant }) => {
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
os_version,
|
||||
os_variant,
|
||||
} as Device);
|
||||
if (!currentOsVersion) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The current os version of the device is not available',
|
||||
);
|
||||
// Just to make TS happy
|
||||
return;
|
||||
}
|
||||
|
||||
return sdk.models.os
|
||||
.getSupportedOsUpdateVersions(device_type, currentOsVersion)
|
||||
.then(hupVersionInfo => {
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
patterns.exitWithExpectedError(
|
||||
'There are no available Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
|
||||
if (options.version != null) {
|
||||
if (!_.includes(hupVersionInfo.versions, options.version)) {
|
||||
patterns.exitWithExpectedError(
|
||||
'The provided version is not in the Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
return options.version;
|
||||
}
|
||||
|
||||
return form.ask({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map(version => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
});
|
||||
})
|
||||
.then(version =>
|
||||
patterns
|
||||
.confirm(
|
||||
options.yes || false,
|
||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||
)
|
||||
.then(() => sdk.models.device.startOsUpdate(uuid, version))
|
||||
.then(() => patterns.awaitDeviceOsUpdate(uuid, version)),
|
||||
);
|
||||
})
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
@ -1,182 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.list =
|
||||
signature: 'envs'
|
||||
description: 'list all environment variables'
|
||||
help: '''
|
||||
Use this command to list all environment variables for
|
||||
a particular application or device.
|
||||
|
||||
This command lists all custom environment variables.
|
||||
If you want to see all environment variables, including private
|
||||
ones used by resin, use the verbose option.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin envs --application MyApp
|
||||
$ resin envs --application MyApp --verbose
|
||||
$ resin envs --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
|
||||
{
|
||||
signature: 'verbose'
|
||||
description: 'show private environment variables'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.environmentVariables.getAllByApplication(options.application)
|
||||
else if options.device?
|
||||
return resin.models.environmentVariables.device.getAll(options.device)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
|
||||
.tap (environmentVariables) ->
|
||||
if _.isEmpty(environmentVariables)
|
||||
throw new Error('No environment variables found')
|
||||
if not options.verbose
|
||||
isSystemVariable = resin.models.environmentVariables.isSystemVariable
|
||||
environmentVariables = _.reject(environmentVariables, isSystemVariable)
|
||||
|
||||
console.log visuals.table.horizontal environmentVariables, [
|
||||
'id'
|
||||
'name'
|
||||
'value'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'env rm <id>'
|
||||
description: 'remove an environment variable'
|
||||
help: '''
|
||||
Use this command to remove an environment variable from an application.
|
||||
|
||||
Don't remove resin specific variables, as things might not work as expected.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
If you want to eliminate a device environment variable, pass the `--device` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rm 215
|
||||
$ resin env rm 215 --yes
|
||||
$ resin env rm 215 --device
|
||||
'''
|
||||
options: [
|
||||
commandOptions.yes
|
||||
commandOptions.booleanDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.remove(params.id)
|
||||
else
|
||||
resin.models.environmentVariables.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'env add <key> [value]'
|
||||
description: 'add an environment variable'
|
||||
help: '''
|
||||
Use this command to add an enviroment variable to an application.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
Use the `--device` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use `--quiet` to remove it.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env add EDITOR vim --application MyApp
|
||||
$ resin env add TERM --application MyApp
|
||||
$ resin env add EDITOR vim --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if not params.value?
|
||||
params.value = process.env[params.key]
|
||||
|
||||
if not params.value?
|
||||
throw new Error("Environment value not found for key: #{params.key}")
|
||||
else
|
||||
console.info("Warning: using #{params.key}=#{params.value} from host environment")
|
||||
|
||||
if options.application?
|
||||
resin.models.environmentVariables.create(options.application, params.key, params.value)
|
||||
else if options.device?
|
||||
resin.models.environmentVariables.device.create(options.device, params.key, params.value)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'env rename <id> <value>'
|
||||
description: 'rename an environment variable'
|
||||
help: '''
|
||||
Use this command to rename an enviroment variable from an application.
|
||||
|
||||
Pass the `--device` boolean option if you want to rename a device environment variable.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rename 376 emacs
|
||||
$ resin env rename 376 emacs --device
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.booleanDevice ]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.update(params.id, params.value)
|
||||
else
|
||||
resin.models.environmentVariables.update(params.id, params.value)
|
||||
.nodeify(done)
|
211
lib/actions/environment-variables.ts
Normal file
211
lib/actions/environment-variables.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
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 * as Bluebird from 'bluebird';
|
||||
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;
|
||||
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 _ = 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(): Bluebird<
|
||||
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 || false,
|
||||
'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 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
|
||||
environment 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 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,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -17,10 +17,14 @@ limitations under the License.
|
||||
_ = require('lodash')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
|
||||
messages = require('../utils/messages')
|
||||
{ getManualSortCompareFunction } = require('../utils/helpers')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
{ getOclifHelpLinePairs } = require('./help_ts')
|
||||
|
||||
parse = (object) ->
|
||||
return _.fromPairs _.map object, (item) ->
|
||||
return _.map object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
@ -39,13 +43,32 @@ indent = (text) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (data) ->
|
||||
console.log indent columnify data,
|
||||
print = (usageDescriptionPairs) ->
|
||||
console.log indent columnify _.fromPairs(usageDescriptionPairs),
|
||||
showHeaders: false
|
||||
minWidth: 35
|
||||
|
||||
manuallySortedPrimaryCommands = [
|
||||
'help',
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'local scan',
|
||||
]
|
||||
|
||||
general = (params, options, done) ->
|
||||
console.log('Usage: resin [COMMAND] [OPTIONS]\n')
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n')
|
||||
console.log(messages.reachingOut)
|
||||
console.log('\nPrimary commands:\n')
|
||||
|
||||
@ -55,27 +78,25 @@ general = (params, options, done) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.plugin
|
||||
return 'plugins'
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
|
||||
print(parse(groupedCommands.primary))
|
||||
print parse(groupedCommands.primary).sort(getManualSortCompareFunction(
|
||||
manuallySortedPrimaryCommands,
|
||||
([signature, description], manualItem) ->
|
||||
signature == manualItem or signature.startsWith("#{manualItem} ")
|
||||
))
|
||||
|
||||
if options.verbose
|
||||
if not _.isEmpty(groupedCommands.plugins)
|
||||
console.log('\nInstalled plugins:\n')
|
||||
print(parse(groupedCommands.plugins))
|
||||
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
print parse(groupedCommands.secondary).concat(getOclifHelpLinePairs()).sort()
|
||||
else
|
||||
console.log('\nRun `resin help --verbose` to list additional commands')
|
||||
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))
|
||||
print parse(capitano.state.globalOptions).sort()
|
||||
|
||||
return done()
|
||||
|
||||
@ -84,7 +105,7 @@ command = (params, options, done) ->
|
||||
return done(error) if error?
|
||||
|
||||
if not command? or command.isWildcard()
|
||||
return done(new Error("Command not found: #{params.command}"))
|
||||
exitWithExpectedError("Command not found: #{params.command}")
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
@ -95,7 +116,7 @@ command = (params, options, done) ->
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
console.log('\nOptions:\n')
|
||||
print(parse(command.options))
|
||||
print parse(command.options).sort()
|
||||
|
||||
return done()
|
||||
|
||||
@ -107,8 +128,8 @@ exports.help =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin help apps
|
||||
$ resin help os download
|
||||
$ balena help apps
|
||||
$ balena help os download
|
||||
'''
|
||||
primary: true
|
||||
options: [
|
||||
|
41
lib/actions/help_ts.ts
Normal file
41
lib/actions/help_ts.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Command } from '@oclif/command';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export function getOclifHelpLinePairs(): Array<[string, string]> {
|
||||
// Although it's tempting to have these oclif commands 'require'd in a
|
||||
// central place, it would impact on performance (CLI start time). An
|
||||
// improvement would probably be to automatically scan the actions-oclif
|
||||
// folder.
|
||||
const EnvAddCmd = require('../actions-oclif/env/add').default;
|
||||
const VersionCmd = require('../actions-oclif/version').default;
|
||||
return [EnvAddCmd, VersionCmd].map(getCmdUsageDescriptionLinePair);
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
|
||||
const usage = (cmd.usage || '').toString().toLowerCase();
|
||||
let description = '';
|
||||
// note: [^] matches any characters (including line breaks), achieving the
|
||||
// same effect as the 's' regex flag which is only supported by Node 9+
|
||||
const matches = /\s*([^]+?)\n[^]*/.exec(cmd.description || '');
|
||||
if (matches && matches.length > 1) {
|
||||
description = _.lowerFirst(_.trimEnd(matches[1], '.'));
|
||||
}
|
||||
return [usage, description];
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,24 +15,28 @@ limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
wizard: require('./wizard')
|
||||
apiKey: require('./api-key')
|
||||
app: require('./app')
|
||||
info: require('./info')
|
||||
auth: require('./auth')
|
||||
device: require('./device')
|
||||
env: require('./environment-variables')
|
||||
tags: require('./tags')
|
||||
keys: require('./keys')
|
||||
logs: require('./logs')
|
||||
local: require('./local')
|
||||
scan: require('./scan')
|
||||
notes: require('./notes')
|
||||
help: require('./help')
|
||||
os: require('./os')
|
||||
settings: require('./settings')
|
||||
config: require('./config')
|
||||
sync: require('./sync')
|
||||
ssh: require('./ssh')
|
||||
internal: require('./internal')
|
||||
build: require('./build')
|
||||
deploy: require('./deploy')
|
||||
util: require('./util')
|
||||
preload: require('./preload')
|
||||
push: require('./push')
|
||||
join: require('./join')
|
||||
leave: require('./leave')
|
||||
tunnel: require('./tunnel')
|
||||
|
@ -1,26 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.version =
|
||||
signature: 'version'
|
||||
description: 'output the version number'
|
||||
help: '''
|
||||
Display the Resin CLI version.
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
packageJSON = require('../../package.json')
|
||||
console.log(packageJSON.version)
|
||||
return done()
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -21,17 +21,68 @@ exports.osInit =
|
||||
signature: 'internal osinit <image> <type> <config>'
|
||||
description: 'do actual init of the device with the preconfigured os image'
|
||||
help: '''
|
||||
Don't use this command directly! Use `resin os initialize <image>` instead.
|
||||
Don't use this command directly! Use `balena os initialize <image>` instead.
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
init = require('resin-device-init')
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
return Promise.try ->
|
||||
config = JSON.parse(params.config)
|
||||
init.initialize(params.image, params.type, config)
|
||||
configPromise = Promise.try -> JSON.parse(params.config)
|
||||
manifestPromise = helpers.getManifest(params.image, params.type)
|
||||
Promise.join configPromise, manifestPromise, (config, manifest) ->
|
||||
init.initialize(params.image, manifest, config)
|
||||
.then(helpers.osProgressHandler)
|
||||
.nodeify(done)
|
||||
|
||||
exports.scanDevices =
|
||||
signature: 'internal scandevices'
|
||||
description: 'scan for local balena-enabled devices and show a picker to choose one'
|
||||
help: '''
|
||||
Don't use this command directly!
|
||||
'''
|
||||
hidden: true
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
return Promise.try ->
|
||||
forms.selectLocalBalenaOsDevice()
|
||||
.then (hostnameOrIp) ->
|
||||
console.error("==> Selected device: #{hostnameOrIp}")
|
||||
.nodeify(done)
|
||||
|
||||
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)
|
||||
|
74
lib/actions/join.ts
Normal file
74
lib/actions/join.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
application?: string;
|
||||
}
|
||||
|
||||
export const join: CommandDefinition<Args, Options> = {
|
||||
signature: 'join [deviceIp]',
|
||||
description:
|
||||
'Promote a local device running balenaOS to join an application on a balena server',
|
||||
help: stripIndent`
|
||||
Use this command to move a local device to an application on another balena server.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
Moving a device between applications on the same server is not supported.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena join
|
||||
$ balena join balena.local
|
||||
$ balena join balena.local --application MyApp
|
||||
$ balena join 192.168.1.25
|
||||
$ balena join 192.168.1.25 --application MyApp
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
alias: 'a',
|
||||
description: 'The name of the application the device should join',
|
||||
},
|
||||
],
|
||||
|
||||
primary: true,
|
||||
|
||||
async action(params, options, done) {
|
||||
const balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.join(logger, sdk, params.deviceIp, options.application);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -24,14 +24,14 @@ exports.list =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin keys
|
||||
$ balena keys
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.getAll().then (keys) ->
|
||||
balena.models.key.getAll().then (keys) ->
|
||||
console.log visuals.table.horizontal keys, [
|
||||
'id'
|
||||
'title'
|
||||
@ -46,14 +46,14 @@ exports.info =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin key 17
|
||||
$ balena key 17
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.key.get(params.id).then (key) ->
|
||||
balena.models.key.get(params.id).then (key) ->
|
||||
console.log visuals.table.vertical key, [
|
||||
'id'
|
||||
'title'
|
||||
@ -61,7 +61,7 @@ exports.info =
|
||||
|
||||
# Since the public key string is long, it might
|
||||
# wrap to lines below, causing the table layout to break.
|
||||
# See https://github.com/resin-io/resin-cli/issues/151
|
||||
# See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key)
|
||||
.nodeify(done)
|
||||
|
||||
@ -69,29 +69,29 @@ exports.remove =
|
||||
signature: 'key rm <id>'
|
||||
description: 'remove a ssh key'
|
||||
help: '''
|
||||
Use this command to remove a SSH key from resin.io.
|
||||
Use this command to remove a SSH key from balena.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin key rm 17
|
||||
$ resin key rm 17 --yes
|
||||
$ balena key rm 17
|
||||
$ balena key rm 17 --yes
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
|
||||
resin.models.key.remove(params.id)
|
||||
balena.models.key.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'key add <name> [path]'
|
||||
description: 'add a SSH key to resin.io'
|
||||
description: 'add a SSH key to balena'
|
||||
help: '''
|
||||
Use this command to associate a new SSH key with your account.
|
||||
|
||||
@ -100,8 +100,8 @@ exports.add =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | resin key add Main
|
||||
$ balena key add Main ~/.ssh/id_rsa.pub
|
||||
$ cat ~/.ssh/id_rsa.pub | balena key add Main
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
@ -109,7 +109,7 @@ exports.add =
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
capitano = require('capitano')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
Promise.try ->
|
||||
return readFileAsync(params.path, encoding: 'utf8') if params.path?
|
||||
@ -119,5 +119,5 @@ exports.add =
|
||||
capitano.utils.getStdin (data) ->
|
||||
return callback(null, data)
|
||||
|
||||
.then(_.partial(resin.models.key.create, params.name))
|
||||
.then(_.partial(balena.models.key.create, params.name))
|
||||
.nodeify(done)
|
||||
|
59
lib/actions/leave.ts
Normal file
59
lib/actions/leave.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
interface Args {
|
||||
deviceIp?: string;
|
||||
}
|
||||
|
||||
export const leave: CommandDefinition<Args, {}> = {
|
||||
signature: 'leave [deviceIp]',
|
||||
description: 'Detach a local device from its balena application',
|
||||
help: stripIndent`
|
||||
Use this command to make a local device leave the balena server it is
|
||||
provisioned on. This effectively makes the device "unmanaged".
|
||||
|
||||
The device entry on the server is preserved after running this command,
|
||||
so the device can subsequently re-join the server if needed.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena leave
|
||||
$ balena leave balena.local
|
||||
$ balena leave 192.168.1.25
|
||||
`,
|
||||
options: [],
|
||||
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
|
||||
async action(params, _options, done) {
|
||||
const balena = await import('balena-sdk');
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = balena.fromSharedOptions();
|
||||
const logger = new Logger();
|
||||
return Bluebird.try(() => {
|
||||
return promote.leave(logger, sdk, params.deviceIp);
|
||||
}).nodeify(done);
|
||||
},
|
||||
};
|
@ -1,19 +1,21 @@
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
Docker = require('docker-toolbelt')
|
||||
form = require('resin-cli-form')
|
||||
chalk = require('chalk')
|
||||
|
||||
dockerUtils = require('../../utils/docker')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
exports.dockerPort = dockerPort = 2375
|
||||
exports.dockerTimeout = dockerTimeout = 2000
|
||||
|
||||
exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container) ->
|
||||
for name in container.Names
|
||||
return false if name.includes('resin_supervisor')
|
||||
return false if (name.includes('resin_supervisor') or name.includes('balena_supervisor'))
|
||||
return true
|
||||
|
||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
form = require('resin-cli-form')
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||
|
||||
# List all containers, including those not running
|
||||
docker.listContainersAsync(all: true)
|
||||
@ -22,23 +24,22 @@ exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor =
|
||||
filterOutSupervisorContainer(container)
|
||||
.then (containers) ->
|
||||
if _.isEmpty(containers)
|
||||
throw new Error("No containers found in #{deviceIp}")
|
||||
exitWithExpectedError("No containers found in #{deviceIp}")
|
||||
|
||||
return form.ask
|
||||
message: 'Select a container'
|
||||
type: 'list'
|
||||
choices: _.map containers, (container) ->
|
||||
containerName = container.Names[0] or 'Untitled'
|
||||
containerName = container.Names?[0] or 'Untitled'
|
||||
shortContainerId = ('' + container.Id).substr(0, 11)
|
||||
containerStatus = container.Status
|
||||
|
||||
return {
|
||||
name: "#{containerName} (#{shortContainerId}) - #{containerStatus}"
|
||||
name: "#{containerName} (#{shortContainerId})"
|
||||
value: container.Id
|
||||
}
|
||||
|
||||
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
|
||||
docker = new Docker(host: deviceIp, port: dockerPort)
|
||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
|
||||
|
||||
container = docker.getContainer(name)
|
||||
container.inspectAsync()
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
BOOT_PARTITION = { primary: 1 }
|
||||
BOOT_PARTITION = 1
|
||||
CONNECTIONS_FOLDER = '/system-connections'
|
||||
|
||||
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
@ -51,6 +51,8 @@ getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
|
||||
type: 'ini'
|
||||
location:
|
||||
path: CONNECTIONS_FOLDER.slice(1)
|
||||
# Reconfix still uses the older resin-image-fs, so still needs an
|
||||
# object-based partition definition.
|
||||
partition: BOOT_PARTITION
|
||||
config_json:
|
||||
type: 'json'
|
||||
@ -151,7 +153,7 @@ prepareConnectionFile = (target) ->
|
||||
if _.includes(files, 'resin-wifi')
|
||||
return null
|
||||
|
||||
# Fresh image, new mode, accoding to https://github.com/resin-os/meta-resin/pull/770/files
|
||||
# Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
|
||||
if _.includes(files, 'resin-sample.ignore')
|
||||
return imagefs.copy
|
||||
image: target
|
||||
@ -191,18 +193,19 @@ removeHostname = (schema) ->
|
||||
|
||||
module.exports =
|
||||
signature: 'local configure <target>'
|
||||
description: '(Re)configure a resinOS drive or image'
|
||||
description: '(Re)configure a balenaOS drive or image'
|
||||
help: '''
|
||||
Use this command to configure or reconfigure a resinOS drive or image.
|
||||
Use this command to configure or reconfigure a balenaOS drive or image.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local configure /dev/sdc
|
||||
$ resin local configure path/to/image.img
|
||||
$ balena local configure /dev/sdc
|
||||
$ balena local configure path/to/image.img
|
||||
'''
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
path = require('path')
|
||||
umount = require('umount')
|
||||
umountAsync = Promise.promisify(umount.umount)
|
||||
isMountedAsync = Promise.promisify(umount.isMounted)
|
||||
@ -215,7 +218,12 @@ module.exports =
|
||||
return if not isMounted
|
||||
umountAsync(params.target)
|
||||
.then (configurationSchema) ->
|
||||
denymount params.target, (cb) ->
|
||||
dmOpts = {}
|
||||
if process.pkg
|
||||
# when running in a standalone pkg install, the 'denymount'
|
||||
# executable is placed on the same folder as process.execPath
|
||||
dmOpts.executablePath = path.join(path.dirname(process.execPath), 'denymount')
|
||||
dmHandler = (cb) ->
|
||||
reconfix.readConfiguration(configurationSchema, params.target)
|
||||
.then(getConfiguration)
|
||||
.then (answers) ->
|
||||
@ -223,6 +231,7 @@ module.exports =
|
||||
removeHostname(configurationSchema)
|
||||
reconfix.writeConfiguration(configurationSchema, answers, params.target)
|
||||
.asCallback(cb)
|
||||
denymount params.target, dmHandler, dmOpts
|
||||
.then ->
|
||||
console.log('Done!')
|
||||
.asCallback(done)
|
||||
|
@ -1,120 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'local flash <image>'
|
||||
description: 'Flash an image to a drive'
|
||||
help: '''
|
||||
Use this command to flash a resinOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local flash path/to/resinos.img
|
||||
$ resin local flash path/to/resinos.img --drive /dev/disk2
|
||||
$ resin local flash path/to/resinos.img --drive /dev/disk2 --yes
|
||||
'''
|
||||
options: [
|
||||
signature: 'yes'
|
||||
boolean: true
|
||||
description: 'confirm non-interactively'
|
||||
alias: 'y'
|
||||
,
|
||||
signature: 'drive'
|
||||
parameter: 'drive'
|
||||
description: 'drive'
|
||||
alias: 'd'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
|
||||
_ = require('lodash')
|
||||
os = require('os')
|
||||
Promise = require('bluebird')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
driveListAsync = Promise.promisify(require('drivelist').list)
|
||||
chalk = require('chalk')
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
imageWrite = require('etcher-image-write')
|
||||
|
||||
form.run [
|
||||
{
|
||||
message: 'Select drive'
|
||||
type: 'drive'
|
||||
name: 'drive'
|
||||
},
|
||||
{
|
||||
message: 'This will erase the selected drive. Are you sure?'
|
||||
type: 'confirm'
|
||||
name: 'yes'
|
||||
default: false
|
||||
}
|
||||
],
|
||||
override:
|
||||
drive: options.drive
|
||||
|
||||
# If `options.yes` is `false`, pass `undefined`,
|
||||
# otherwise the question will not be asked because
|
||||
# `false` is a defined value.
|
||||
yes: options.yes || undefined
|
||||
|
||||
# TODO: dedupe with the resin-device-operations
|
||||
.then (answers) ->
|
||||
if answers.yes isnt true
|
||||
console.log(chalk.red.bold('Aborted image flash'))
|
||||
process.exit(0)
|
||||
|
||||
driveListAsync().then (drives) ->
|
||||
selectedDrive = _.find(drives, device: answers.drive)
|
||||
|
||||
if not selectedDrive?
|
||||
throw new Error("Drive not found: #{answers.drive}")
|
||||
|
||||
return selectedDrive
|
||||
.then (selectedDrive) ->
|
||||
progressBars =
|
||||
write: new visuals.Progress('Flashing')
|
||||
check: new visuals.Progress('Validating')
|
||||
|
||||
umountAsync(selectedDrive.device).then ->
|
||||
Promise.props
|
||||
imageSize: fs.statAsync(params.image).get('size'),
|
||||
imageStream: Promise.resolve(fs.createReadStream(params.image))
|
||||
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
|
||||
.then (results) ->
|
||||
imageWrite.write
|
||||
fd: results.driveFileDescriptor
|
||||
device: selectedDrive.raw
|
||||
size: selectedDrive.size
|
||||
,
|
||||
stream: results.imageStream,
|
||||
size: results.imageSize
|
||||
,
|
||||
check: true
|
||||
.then (writer) ->
|
||||
new Promise (resolve, reject) ->
|
||||
writer.on 'progress', (state) ->
|
||||
progressBars[state.type].update(state)
|
||||
writer.on('error', reject)
|
||||
writer.on('done', resolve)
|
||||
.then ->
|
||||
if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
return umountAsync(selectedDrive.device)
|
||||
.asCallback(done)
|
120
lib/actions/local/flash.ts
Normal file
120
lib/actions/local/flash.ts
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as SDK from 'etcher-sdk';
|
||||
|
||||
async function getDrive(options: {
|
||||
drive?: string;
|
||||
}): Promise<SDK.sourceDestination.BlockDevice> {
|
||||
const sdk = await import('etcher-sdk');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
let drive: SDK.sourceDestination.BlockDevice;
|
||||
if (options.drive !== undefined) {
|
||||
const d = scanner.getBy('device', options.drive);
|
||||
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
|
||||
throw new Error(`Drive not found: ${options.drive}`);
|
||||
}
|
||||
drive = d;
|
||||
} else {
|
||||
const { DriveList } = await import('../../utils/visuals/drive-list');
|
||||
const driveList = new DriveList(scanner);
|
||||
drive = await driveList.run();
|
||||
}
|
||||
scanner.stop();
|
||||
return drive;
|
||||
}
|
||||
|
||||
export const flash: CommandDefinition<
|
||||
{ image: string },
|
||||
{ drive: string; yes: boolean }
|
||||
> = {
|
||||
signature: 'local flash <image>',
|
||||
description: 'Flash an image to a drive',
|
||||
help: stripIndent`
|
||||
Use this command to flash a balenaOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local flash path/to/balenaos.img[.zip|.gz|.bz2|.xz]
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'yes',
|
||||
boolean: true,
|
||||
description: 'confirm non-interactively',
|
||||
alias: 'y',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
parameter: 'drive',
|
||||
description: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
async action(params, options) {
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const form = await import('resin-cli-form');
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
|
||||
const drive = await getDrive(options);
|
||||
|
||||
const yes =
|
||||
options.yes ||
|
||||
(await form.ask({
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false,
|
||||
}));
|
||||
if (yes !== true) {
|
||||
console.log(chalk.red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const file = new sourceDestination.File(
|
||||
params.image,
|
||||
sourceDestination.File.OpenFlags.Read,
|
||||
);
|
||||
const source = await file.getInnerSource();
|
||||
|
||||
const progressBars: { [key: string]: any } = {
|
||||
flashing: new visuals.Progress('Flashing'),
|
||||
verifying: new visuals.Progress('Validating'),
|
||||
};
|
||||
|
||||
await multiWrite.pipeSourceToDestinations(
|
||||
source,
|
||||
[drive],
|
||||
(_, error) => {
|
||||
// onFail
|
||||
console.log(chalk.red.bold(error.message));
|
||||
},
|
||||
(progress: SDK.multiWrite.MultiDestinationProgress) => {
|
||||
// onProgress
|
||||
progressBars[progress.type].update(progress);
|
||||
},
|
||||
true, // verify
|
||||
);
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -15,9 +15,4 @@ limitations under the License.
|
||||
###
|
||||
|
||||
exports.configure = require('./configure')
|
||||
exports.flash = require('./flash')
|
||||
exports.logs = require('./logs')
|
||||
exports.scan = require('./scan')
|
||||
exports.ssh = require('./ssh')
|
||||
exports.push = require('./push')
|
||||
exports.stop = require('./stop')
|
||||
exports.flash = require('./flash').flash
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -22,16 +22,16 @@ limitations under the License.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local logs [deviceIp]'
|
||||
description: 'Get or attach to logs of a running container on a resinOS device'
|
||||
description: 'Get or attach to logs of a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local logs
|
||||
$ resin local logs -f
|
||||
$ resin local logs 192.168.1.10
|
||||
$ resin local logs 192.168.1.10 -f
|
||||
$ resin local logs 192.168.1.10 -f --app-name myapp
|
||||
$ balena local logs
|
||||
$ balena local logs -f
|
||||
$ balena local logs 192.168.1.10
|
||||
$ balena local logs 192.168.1.10 -f
|
||||
$ balena local logs 192.168.1.10 -f --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'follow'
|
||||
@ -47,12 +47,12 @@ module.exports =
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
{ forms } = require('resin-sync')
|
||||
{ forms } = require('balena-sync')
|
||||
{ selectContainerFromDevice, pipeContainerStream } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
if not options['app-name']?
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,43 +14,57 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
# Loads '.resin-sync.yml' configuration from 'source' directory.
|
||||
# Loads '.balena-sync.yml' configuration from 'source' directory.
|
||||
# Returns the configuration object on success
|
||||
#
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
resinPush = require('resin-sync').capitano('resin-toolbox')
|
||||
balenaPush = require('balena-sync').capitano('balena-toolbox')
|
||||
originalAction = balenaPush.action
|
||||
|
||||
# TODO: This is a temporary workaround to reuse the existing `rdt push`
|
||||
# capitano frontend in `resin local push`.
|
||||
# capitano frontend in `balena local push`.
|
||||
|
||||
resinPushHelp = '''
|
||||
Warning: 'resin local push' requires an openssh-compatible client and 'rsync' to
|
||||
be correctly installed in your shell environment. For more information (including
|
||||
Windows support) please check the README here: https://github.com/resin-io/resin-cli
|
||||
# coffeelint: disable-next-line ("Line ends with trailing whitespace")
|
||||
deprecationMsg = '''
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
Deprecation notice: `balena local push` is deprecated and will be removed in a
|
||||
future release of the CLI. Please use `balena push <ipAddress>` instead.
|
||||
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
Use this command to push your local changes to a container on a LAN-accessible resinOS device on the fly.
|
||||
'''
|
||||
|
||||
balenaPushHelp = """#{deprecationMsg}
|
||||
Use this command to push your local changes to a container on a LAN-accessible
|
||||
balenaOS device on the fly.
|
||||
|
||||
This command requires an openssh-compatible 'ssh' client and 'rsync' to be
|
||||
available in the executable PATH of the shell environment. For more information
|
||||
(including Windows support) please check the README at:
|
||||
https://github.com/balena-io/balena-cli
|
||||
|
||||
If `Dockerfile` or any file in the 'build-triggers' list is changed,
|
||||
a new container will be built and run on your device.
|
||||
If not, changes will simply be synced with `rsync` into the application container.
|
||||
|
||||
After every 'resin local push' the updated settings will be saved in
|
||||
'<source>/.resin-sync.yml' and will be used in later invocations. You can
|
||||
also change any option by editing '.resin-sync.yml' directly.
|
||||
After every 'balena local push' the updated settings will be saved in
|
||||
'<source>/.balena-sync.yml' and will be used in later invocations. You can
|
||||
also change any option by editing '.balena-sync.yml' directly.
|
||||
|
||||
Here is an example '.resin-sync.yml' :
|
||||
Here is an example '.balena-sync.yml' :
|
||||
|
||||
$ cat $PWD/.resin-sync.yml
|
||||
destination: '/usr/src/app'
|
||||
before: 'echo Hello'
|
||||
after: 'echo Done'
|
||||
ignore:
|
||||
- .git
|
||||
- node_modules/
|
||||
$ cat $PWD/.balena-sync.yml
|
||||
local_balenaos:
|
||||
app-name: local-app
|
||||
build-triggers:
|
||||
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
|
||||
environment:
|
||||
- MY_VARIABLE=123
|
||||
|
||||
Command line options have precedence over the ones saved in '.resin-sync.yml'.
|
||||
|
||||
Command line options have precedence over the ones saved in '.balena-sync.yml'.
|
||||
|
||||
If '.gitignore' is found in the source directory then all explicitly listed files will be
|
||||
excluded when using rsync to update the container. You can choose to change this default behavior with the
|
||||
@ -58,19 +72,24 @@ resinPushHelp = '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local push
|
||||
$ resin local push --app-name test-server --build-triggers package.json,requirements.txt
|
||||
$ resin local push --force-build
|
||||
$ resin local push --force-build --skip-logs
|
||||
$ resin local push --ignore lib/
|
||||
$ resin local push --verbose false
|
||||
$ resin local push 192.168.2.10 --source . --destination /usr/src/app
|
||||
$ resin local push 192.168.2.10 -s /home/user/myResinProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
|
||||
'''
|
||||
$ balena local push
|
||||
$ balena local push --app-name test-server --build-triggers package.json,requirements.txt
|
||||
$ balena local push --force-build
|
||||
$ balena local push --force-build --skip-logs
|
||||
$ balena local push --ignore lib/
|
||||
$ balena local push --verbose false
|
||||
$ balena local push 192.168.2.10 --source . --destination /usr/src/app
|
||||
$ balena local push 192.168.2.10 -s /home/user/balenaProject -d /usr/src/app --before 'echo Hello' --after 'echo Done'
|
||||
"""
|
||||
|
||||
|
||||
module.exports = _.assign resinPush,
|
||||
|
||||
module.exports = _.assign balenaPush,
|
||||
signature: 'local push [deviceIp]'
|
||||
help: resinPushHelp
|
||||
primary: true
|
||||
description: '[deprecated: use "balena push ipAddress"] ' + balenaPush.description
|
||||
help: balenaPushHelp
|
||||
primary: false
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
console.log deprecationMsg
|
||||
originalAction(params, options, done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,28 +14,34 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ hostOSAccess } = require('../command-options')
|
||||
_ = require('lodash')
|
||||
|
||||
localHostOSAccessOption = _.cloneDeep(hostOSAccess)
|
||||
localHostOSAccessOption.description = 'get a shell into the host OS'
|
||||
|
||||
module.exports =
|
||||
signature: 'local ssh [deviceIp]'
|
||||
description: 'Get a shell into a resinOS device'
|
||||
description: 'Get a shell into a balenaOS device'
|
||||
help: '''
|
||||
Warning: 'resin local ssh' requires an openssh-compatible client to be correctly
|
||||
Warning: 'balena local ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check the README here: https://github.com/resin-io/resin-cli
|
||||
support) please check the README here: https://github.com/balena-io/balena-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
The '--host' option will get you a shell into the Host OS of the resinOS device.
|
||||
The '--host' option will get you a shell into the Host OS of the balenaOS device.
|
||||
No option will return a list of containers to enter or you can explicitly select
|
||||
one by passing its name to the --container option
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local ssh
|
||||
$ resin local ssh --host
|
||||
$ resin local ssh --container chaotic_water
|
||||
$ resin local ssh --container chaotic_water --port 22222
|
||||
$ resin local ssh --verbose
|
||||
$ balena local ssh
|
||||
$ balena local ssh --host
|
||||
$ balena local ssh --container chaotic_water
|
||||
$ balena local ssh --container chaotic_water --port 22222
|
||||
$ balena local ssh --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
@ -43,11 +49,7 @@ module.exports =
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'host'
|
||||
boolean: true
|
||||
description: 'get a shell into the host OS'
|
||||
alias: 's'
|
||||
,
|
||||
localHostOSAccessOption,
|
||||
signature: 'container'
|
||||
parameter: 'container'
|
||||
default: null
|
||||
@ -64,11 +66,13 @@ module.exports =
|
||||
child_process = require('child_process')
|
||||
Promise = require 'bluebird'
|
||||
_ = require('lodash')
|
||||
{ forms } = require('resin-sync')
|
||||
{ forms } = require('balena-sync')
|
||||
|
||||
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
|
||||
{ exitWithExpectedError } = require('../../utils/patterns')
|
||||
|
||||
if (options.host is true and options.container?)
|
||||
throw new Error('Please pass either --host or --container option')
|
||||
exitWithExpectedError('Please pass either --host or --container option')
|
||||
|
||||
if not options.port?
|
||||
options.port = 22222
|
||||
@ -77,7 +81,7 @@ module.exports =
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (deviceIp) ->
|
||||
_.assign(options, { deviceIp })
|
||||
@ -101,7 +105,8 @@ module.exports =
|
||||
|
||||
if not options.host
|
||||
shellCmd = '''/bin/sh -c $"'if [ -e /bin/bash ]; then exec /bin/bash; else exec /bin/sh; fi'"'''
|
||||
command += " docker exec -ti #{container} #{shellCmd}"
|
||||
dockerCmd = "'$(if [ -f /usr/bin/balena ]; then echo \"balena\"; else echo \"docker\"; fi)'"
|
||||
command += " #{dockerCmd} exec -ti #{container} #{shellCmd}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -22,16 +22,16 @@ limitations under the License.
|
||||
#
|
||||
module.exports =
|
||||
signature: 'local stop [deviceIp]'
|
||||
description: 'Stop a running container on a resinOS device'
|
||||
description: 'Stop a running container on a balenaOS device'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local stop
|
||||
$ resin local stop --app-name myapp
|
||||
$ resin local stop --all
|
||||
$ resin local stop 192.168.1.10
|
||||
$ resin local stop 192.168.1.10 --app-name myapp
|
||||
$ balena local stop
|
||||
$ balena local stop --app-name myapp
|
||||
$ balena local stop --all
|
||||
$ balena local stop 192.168.1.10
|
||||
$ balena local stop 192.168.1.10 --app-name myapp
|
||||
'''
|
||||
options: [
|
||||
signature: 'all'
|
||||
@ -47,15 +47,15 @@ module.exports =
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
chalk = require('chalk')
|
||||
{ forms, config, ResinLocalDockerUtils } = require('resin-sync')
|
||||
{ forms, config, BalenaLocalDockerUtils } = require('balena-sync')
|
||||
{ selectContainerFromDevice, filterOutSupervisorContainer } = require('./common')
|
||||
|
||||
Promise.try ->
|
||||
if not params.deviceIp?
|
||||
return forms.selectLocalResinOsDevice()
|
||||
return forms.selectLocalBalenaOsDevice()
|
||||
return params.deviceIp
|
||||
.then (@deviceIp) =>
|
||||
@docker = new ResinLocalDockerUtils(@deviceIp)
|
||||
@docker = new BalenaLocalDockerUtils(@deviceIp)
|
||||
|
||||
if options.all
|
||||
# Only list running containers
|
||||
@ -67,7 +67,7 @@ module.exports =
|
||||
@docker.stopContainer(Id)
|
||||
|
||||
ymlConfig = config.load()
|
||||
@appName = options['app-name'] ? ymlConfig['local_resinos']?['app-name']
|
||||
@appName = options['app-name'] ? ymlConfig['local_balenaos']?['app-name']
|
||||
@docker.checkForRunningContainer(@appName)
|
||||
.then (isRunning) =>
|
||||
if not isRunning
|
||||
|
@ -1,69 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'logs <uuid>'
|
||||
description: 'show device logs'
|
||||
help: '''
|
||||
Use this command to show logs for a specific device.
|
||||
|
||||
By default, the command prints all log messages and exit.
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the `--tail` option.
|
||||
|
||||
Note that for now you need to provide the whole UUID for this command to work correctly.
|
||||
|
||||
This is due to some technical limitations that we plan to address soon.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin logs 23c73a1
|
||||
$ resin logs 23c73a1
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'tail'
|
||||
description: 'continuously stream output'
|
||||
boolean: true
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
moment = require('moment')
|
||||
|
||||
printLine = (line) ->
|
||||
timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)')
|
||||
console.log("#{timestamp} #{line.message}")
|
||||
|
||||
promise = resin.logs.history(params.uuid).each(printLine)
|
||||
|
||||
if not options.tail
|
||||
|
||||
# PubNub keeps the process alive after a history query.
|
||||
# Until this is fixed, we force the process to exit.
|
||||
# This of course prevents this command to be used programatically
|
||||
return promise.catch(done).finally ->
|
||||
process.exit(0)
|
||||
|
||||
promise.then ->
|
||||
resin.logs.subscribe(params.uuid).then (logs) ->
|
||||
logs.on('line', printLine)
|
||||
logs.on('error', done)
|
||||
.catch(done)
|
184
lib/actions/logs.ts
Normal file
184
lib/actions/logs.ts
Normal file
@ -0,0 +1,184 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import { validateDotLocalUrl } from '../utils/validation';
|
||||
|
||||
type CloudLog =
|
||||
| {
|
||||
isSystem: false;
|
||||
serviceId: number;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
}
|
||||
| {
|
||||
isSystem: true;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const logs: CommandDefinition<
|
||||
{
|
||||
uuidOrDevice: string;
|
||||
},
|
||||
{
|
||||
tail?: boolean;
|
||||
service?: [string] | string;
|
||||
system?: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'logs <uuidOrDevice>',
|
||||
description: 'show device logs',
|
||||
help: stripIndent`
|
||||
Use this command to show logs for a specific device.
|
||||
|
||||
By default, the command prints all log messages and exits.
|
||||
|
||||
To continuously stream output, and see new logs in real time, use the \`--tail\` option.
|
||||
|
||||
If an IP or .local address is passed to this command, logs are displayed from
|
||||
a local mode device with that address. Note that --tail is implied
|
||||
when this command is provided a local mode device.
|
||||
|
||||
Logs from a single service can be displayed with the --service flag. Just system logs
|
||||
can be shown with the --system flag. Note that these flags can be used together.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena logs 23c73a1
|
||||
$ balena logs 23c73a1 --tail
|
||||
|
||||
$ balena logs 192.168.0.31
|
||||
$ balena logs 192.168.0.31 --service my-service
|
||||
$ balena logs 192.168.0.31 --service my-service-1 --service my-service-2
|
||||
|
||||
$ balena logs 23c73a1.local --system
|
||||
$ balena logs 23c73a1.local --system --service my-service`,
|
||||
options: [
|
||||
{
|
||||
signature: 'tail',
|
||||
description: 'continuously stream output',
|
||||
boolean: true,
|
||||
alias: 't',
|
||||
},
|
||||
{
|
||||
signature: 'service',
|
||||
description: stripIndent`
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system or other --service flags.`,
|
||||
parameter: 'service',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'system',
|
||||
alias: 'S',
|
||||
boolean: true,
|
||||
description:
|
||||
'Only show system logs. This can be used in combination with --service.',
|
||||
},
|
||||
],
|
||||
primary: true,
|
||||
async action(params, options, done) {
|
||||
normalizeUuidProp(params);
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const isArray = await import('lodash/isArray');
|
||||
const { serviceIdToName } = await import('../utils/cloud');
|
||||
const { displayDeviceLogs, displayLogObject } = await import(
|
||||
'../utils/device/logs'
|
||||
);
|
||||
const { validateIPAddress } = await import('../utils/validation');
|
||||
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const Logger = await import('../utils/logger');
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
|
||||
const displayCloudLog = async (line: CloudLog) => {
|
||||
if (!line.isSystem) {
|
||||
let serviceName = await serviceIdToName(balena, line.serviceId);
|
||||
if (serviceName == null) {
|
||||
serviceName = 'Unknown service';
|
||||
}
|
||||
displayLogObject(
|
||||
{ serviceName, ...line },
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
} else {
|
||||
displayLogObject(
|
||||
line,
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
validateIPAddress(params.uuidOrDevice) ||
|
||||
validateDotLocalUrl(params.uuidOrDevice)
|
||||
) {
|
||||
const { DeviceAPI } = await import('../utils/device/api');
|
||||
const deviceApi = new DeviceAPI(logger, params.uuidOrDevice);
|
||||
logger.logDebug('Checking we can access device');
|
||||
try {
|
||||
await deviceApi.ping();
|
||||
} catch (e) {
|
||||
exitWithExpectedError(
|
||||
new Error(
|
||||
`Cannot access local mode device at address ${params.uuidOrDevice}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const logStream = await deviceApi.getLogStream();
|
||||
displayDeviceLogs(
|
||||
logStream,
|
||||
logger,
|
||||
options.system || false,
|
||||
servicesToDisplay,
|
||||
);
|
||||
} else {
|
||||
exitIfNotLoggedIn();
|
||||
if (options.tail) {
|
||||
return balena.logs
|
||||
.subscribe(params.uuidOrDevice, { count: 100 })
|
||||
.then(function(logStream) {
|
||||
logStream.on('line', displayCloudLog);
|
||||
logStream.on('error', done);
|
||||
})
|
||||
.catch(done);
|
||||
} else {
|
||||
return balena.logs
|
||||
.history(params.uuidOrDevice)
|
||||
.each(displayCloudLog)
|
||||
.catch(done);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
exports.set =
|
||||
signature: 'note <|note>'
|
||||
description: 'set a device note'
|
||||
@ -22,12 +24,12 @@ exports.set =
|
||||
|
||||
If note command isn't passed, the tool attempts to read from `stdin`.
|
||||
|
||||
To view the notes, use $ resin device <uuid>.
|
||||
To view the notes, use $ balena device <uuid>.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | resin note --device 7cf02a6
|
||||
$ balena note "My useful note" --device 7cf02a6
|
||||
$ cat note.txt | balena note --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
signature: 'device'
|
||||
@ -38,13 +40,16 @@ exports.set =
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
if _.isEmpty(params.note)
|
||||
throw new Error('Missing note content')
|
||||
exitWithExpectedError('Missing note content')
|
||||
|
||||
resin.models.device.note(options.device, params.note)
|
||||
balena.models.device.note(options.device, params.note)
|
||||
.nodeify(done)
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
_ = require('lodash')
|
||||
{ normalizeUuidProp } = require('../utils/normalization')
|
||||
|
||||
formatVersion = (v, isRecommended) ->
|
||||
result = "v#{v}"
|
||||
@ -30,9 +31,9 @@ resolveVersion = (deviceType, version) ->
|
||||
return Promise.resolve(version)
|
||||
|
||||
form = require('resin-cli-form')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(deviceType)
|
||||
balena.models.os.getSupportedVersions(deviceType)
|
||||
.then ({ versions, recommended }) ->
|
||||
choices = versions.map (v) ->
|
||||
value: v
|
||||
@ -46,19 +47,19 @@ resolveVersion = (deviceType, version) ->
|
||||
|
||||
exports.versions =
|
||||
signature: 'os versions <type>'
|
||||
description: 'show the available resinOS versions for the given device type'
|
||||
description: 'show the available balenaOS versions for the given device type'
|
||||
help: '''
|
||||
Use this command to show the available resinOS versions for a certain device type.
|
||||
Check available types with `resin devices supported`
|
||||
Use this command to show the available balenaOS versions for a certain device type.
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
Example:
|
||||
|
||||
$ resin os versions raspberrypi3
|
||||
$ balena os versions raspberrypi3
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.models.os.getSupportedVersions(params.type)
|
||||
balena.models.os.getSupportedVersions(params.type)
|
||||
.then ({ versions, recommended }) ->
|
||||
versions.forEach (v) ->
|
||||
console.log(formatVersion(v, v is recommended))
|
||||
@ -68,7 +69,7 @@ exports.download =
|
||||
description: 'download an unconfigured os image'
|
||||
help: '''
|
||||
Use this command to download an unconfigured os image for a certain device type.
|
||||
Check available types with `resin devices supported`
|
||||
Check available types with `balena devices supported`
|
||||
|
||||
If version is not specified the newest stable (non-pre-release) version of OS
|
||||
is downloaded if available, or the newest version otherwise (if all existing
|
||||
@ -79,12 +80,12 @@ exports.download =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ resin os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version 1.24.1
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version ^1.20.0
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version latest
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version default
|
||||
$ balena os download raspberrypi3 -o ../foo/bar/raspberry-pi.img --version menu
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -95,14 +96,14 @@ exports.download =
|
||||
alias: 'o'
|
||||
required: 'You have to specify the output location'
|
||||
}
|
||||
commandOptions.osVersion
|
||||
commandOptions.osVersionOrSemver
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
unzip = require('unzip2')
|
||||
fs = require('fs')
|
||||
rindle = require('rindle')
|
||||
manager = require('resin-image-manager')
|
||||
manager = require('balena-image-manager')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
console.info("Getting device operating system for #{params.type}")
|
||||
@ -146,33 +147,39 @@ exports.download =
|
||||
console.info('The image was downloaded successfully')
|
||||
.nodeify(done)
|
||||
|
||||
buildConfig = (image, deviceType, advanced = false) ->
|
||||
buildConfigForDeviceType = (deviceType, advanced = false) ->
|
||||
form = require('resin-cli-form')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getManifest(image, deviceType)
|
||||
.get('options')
|
||||
.then (questions) ->
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
questions = deviceType.options
|
||||
if not advanced
|
||||
advancedGroup = _.find questions,
|
||||
name: 'advanced'
|
||||
isGroup: true
|
||||
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
if advancedGroup?
|
||||
override = helpers.getGroupDefaults(advancedGroup)
|
||||
|
||||
return form.run(questions, { override })
|
||||
return form.run(questions, { override })
|
||||
|
||||
buildConfig = (image, deviceTypeSlug, advanced = false) ->
|
||||
Promise = require('bluebird')
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
Promise.resolve(helpers.getManifest(image, deviceTypeSlug))
|
||||
.then (deviceTypeManifest) ->
|
||||
buildConfigForDeviceType(deviceTypeManifest, advanced)
|
||||
|
||||
exports.buildConfig =
|
||||
signature: 'os build-config <image> <device-type>'
|
||||
description: 'build the OS config and save it to the JSON file'
|
||||
help: '''
|
||||
Use this command to prebuild the OS config once and skip the interactive part of `resin os configure`.
|
||||
Use this command to prebuild the OS config once and skip the interactive part of `balena os configure`.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ resin os configure ../path/rpi3.img 7cf02a6 --config "$(cat rpi3-config.json)"
|
||||
$ balena os build-config ../path/rpi3.img raspberrypi3 --output rpi3-config.json
|
||||
$ balena os configure ../path/rpi3.img --device 7cf02a6 --config rpi3-config.json
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
@ -196,23 +203,32 @@ exports.buildConfig =
|
||||
.nodeify(done)
|
||||
|
||||
exports.configure =
|
||||
signature: 'os configure <image> [uuid] [deviceApiKey]'
|
||||
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.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+.
|
||||
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.
|
||||
|
||||
This comand still supports the *deprecated* format where the UUID and optionally device key
|
||||
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 remove in a future release.
|
||||
--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:
|
||||
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6
|
||||
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
|
||||
$ resin os configure ../path/rpi.img --app MyApp
|
||||
$ 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: [
|
||||
@ -220,18 +236,21 @@ exports.configure =
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
commandOptions.optionalDeviceType
|
||||
commandOptions.optionalOsVersion
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin os build-config`'
|
||||
description: 'path to the config JSON file, see `balena os build-config`'
|
||||
parameter: 'config'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
normalizeUuidProp(options, 'device')
|
||||
fs = require('fs')
|
||||
Promise = require('bluebird')
|
||||
readFileAsync = Promise.promisify(fs.readFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
init = require('resin-device-init')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
init = require('balena-device-init')
|
||||
helpers = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
@ -239,48 +258,84 @@ exports.configure =
|
||||
if _.filter([
|
||||
options.device
|
||||
options.application
|
||||
params.uuid
|
||||
]).length != 1
|
||||
patterns.expectedError '''
|
||||
patterns.exitWithExpectedError '''
|
||||
To configure an image, you must provide exactly one of:
|
||||
|
||||
* A device, with --device <uuid>
|
||||
* An application, with --app <appname>
|
||||
* [Deprecated] A device, passing its uuid directly on the command line
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ resin help os configure
|
||||
$ balena help os configure
|
||||
'''
|
||||
if params.uuid
|
||||
console.warn(
|
||||
'Directly passing a UUID to `resin os configure` is deprecated. Pass it with --uuid <uuid> instead.' +
|
||||
if params.deviceApiKey
|
||||
' Device api keys can be passed with --deviceApiKey.\n'
|
||||
else '\n'
|
||||
)
|
||||
|
||||
uuid = options.device || params.uuid
|
||||
deviceApiKey = options.deviceApiKey || params.deviceApiKey
|
||||
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'
|
||||
|
||||
resin.models[configurationResourceType].get(uuid || options.application)
|
||||
balena.models[configurationResourceType].get(uuid || options.application)
|
||||
.then (appOrDevice) ->
|
||||
Promise.try ->
|
||||
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 buildConfig(params.image, appOrDevice.device_type, options.advanced)
|
||||
.then (answers) ->
|
||||
(if configurationResourceType == 'device'
|
||||
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
|
||||
else
|
||||
generateApplicationConfig(appOrDevice, answers)
|
||||
).then (config) ->
|
||||
init.configure(params.image, appOrDevice.device_type, config, answers)
|
||||
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)
|
||||
|
||||
@ -299,14 +354,14 @@ exports.initialize =
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
$ balena os initialize ../path/rpi.img --type 'raspberry-pi'
|
||||
"""
|
||||
permission: 'user'
|
||||
options: [
|
||||
commandOptions.yes
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
description: 'device type (Check available types with `balena devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
@ -325,7 +380,7 @@ exports.initialize =
|
||||
|
||||
#{INIT_WARNING_MESSAGE}
|
||||
""")
|
||||
helpers.getManifest(params.image, options.type)
|
||||
Promise.resolve(helpers.getManifest(params.image, options.type))
|
||||
.then (manifest) ->
|
||||
return manifest.initialization?.options
|
||||
.then (questions) ->
|
||||
@ -352,7 +407,7 @@ exports.initialize =
|
||||
.then (answers) ->
|
||||
return if not answers.drive?
|
||||
|
||||
# TODO: resin local makes use of ejectAsync, see below
|
||||
# TODO: balena local makes use of ejectAsync, see below
|
||||
# DO we need this / should we do that here?
|
||||
|
||||
# getDrive = (drive) ->
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,42 +14,70 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
LATEST = 'latest'
|
||||
allDeviceTypes = undefined
|
||||
|
||||
isCurrent = (commit) ->
|
||||
return commit == 'latest' or commit == 'current'
|
||||
|
||||
getDeviceTypes = ->
|
||||
Bluebird = require('bluebird')
|
||||
_ = require('lodash')
|
||||
if allDeviceTypes != undefined
|
||||
return Bluebird.resolve(allDeviceTypes)
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
balena.models.config.getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
_.sortBy(deviceTypes, 'name')
|
||||
.tap (dt) ->
|
||||
allDeviceTypes = dt
|
||||
|
||||
getDeviceTypesWithSameArch = (deviceTypeSlug) ->
|
||||
_ = require('lodash')
|
||||
|
||||
getDeviceTypes()
|
||||
.then (deviceTypes) ->
|
||||
deviceType = _.find(deviceTypes, slug: deviceTypeSlug)
|
||||
_(deviceTypes).filter(arch: deviceType.arch).map('slug').value()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds = (deviceType) ->
|
||||
preload = require('resin-preload')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
preload = require('balena-preload')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
|
||||
resin.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
filter:
|
||||
device_type: deviceType
|
||||
build:
|
||||
$any:
|
||||
$alias: 'b'
|
||||
$expr:
|
||||
b:
|
||||
status: 'success'
|
||||
expand: preload.applicationExpandOptions
|
||||
select: [ 'id', 'app_name', 'device_type', 'commit' ]
|
||||
orderby: 'app_name asc'
|
||||
getDeviceTypesWithSameArch(deviceType)
|
||||
.then (deviceTypes) ->
|
||||
balena.pine.get
|
||||
resource: 'my_application'
|
||||
options:
|
||||
$filter:
|
||||
device_type:
|
||||
$in: deviceTypes
|
||||
owns__release:
|
||||
$any:
|
||||
$alias: 'r'
|
||||
$expr:
|
||||
r:
|
||||
status: 'success'
|
||||
$expand: preload.applicationExpandOptions
|
||||
$select: [ 'id', 'app_name', 'device_type', 'commit', 'should_track_latest_release' ]
|
||||
$orderby: 'app_name asc'
|
||||
|
||||
selectApplication = (deviceType) ->
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and builds.')
|
||||
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
|
||||
applicationInfoSpinner.start()
|
||||
|
||||
getApplicationsWithSuccessfulBuilds(deviceType)
|
||||
.then (applications) ->
|
||||
applicationInfoSpinner.stop()
|
||||
if applications.length == 0
|
||||
expectedError("You have no apps with successful builds for a '#{deviceType}' device type.")
|
||||
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
|
||||
form.ask
|
||||
message: 'Select an application'
|
||||
type: 'list'
|
||||
@ -57,36 +85,41 @@ selectApplication = (deviceType) ->
|
||||
name: app.app_name
|
||||
value: app
|
||||
|
||||
selectApplicationCommit = (builds) ->
|
||||
selectApplicationCommit = (releases) ->
|
||||
form = require('resin-cli-form')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if builds.length == 0
|
||||
expectedError('This application has no successful builds.')
|
||||
DEFAULT_CHOICE = {'name': LATEST, 'value': LATEST}
|
||||
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
|
||||
name: "#{build.push_timestamp} - #{build.commit_hash}"
|
||||
value: build.commit_hash
|
||||
if releases.length == 0
|
||||
exitWithExpectedError('This application has no successful releases.')
|
||||
DEFAULT_CHOICE = { 'name': 'current', 'value': 'current' }
|
||||
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
|
||||
name: "#{release.end_timestamp} - #{release.commit}"
|
||||
value: release.commit
|
||||
return form.ask
|
||||
message: 'Select a build'
|
||||
message: 'Select a release'
|
||||
type: 'list'
|
||||
default: LATEST
|
||||
default: 'current'
|
||||
choices: choices
|
||||
|
||||
offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
offerToDisableAutomaticUpdates = (application, commit, pinDevice) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
form = require('resin-cli-form')
|
||||
|
||||
if commit == LATEST or not application.should_track_latest_release
|
||||
if isCurrent(commit) or not application.should_track_latest_release or pinDevice
|
||||
return Promise.resolve()
|
||||
message = '''
|
||||
|
||||
This application is set to automatically update all devices to the latest available version.
|
||||
This application is set to automatically update all devices to the current version.
|
||||
This might be unexpected behaviour: with this enabled, the preloaded device will still
|
||||
download and install the latest build once it is online.
|
||||
download and install the current release once it is online.
|
||||
|
||||
Do you want to disable automatic updates for this application?
|
||||
|
||||
Warning: To re-enable this requires direct api calls,
|
||||
see https://balena.io/docs/reference/api/resources/device/#set-device-to-release
|
||||
|
||||
Alternatively you can pass the --pin-device-to-release flag to pin only this device to the selected release.
|
||||
'''
|
||||
form.ask
|
||||
message: message,
|
||||
@ -94,69 +127,87 @@ offerToDisableAutomaticUpdates = (application, commit) ->
|
||||
.then (update) ->
|
||||
if not update
|
||||
return
|
||||
resin.pine.patch
|
||||
balena.pine.patch
|
||||
resource: 'application'
|
||||
id: application.id
|
||||
body:
|
||||
should_track_latest_release: false
|
||||
|
||||
preloadOptions = dockerUtils.appendConnectionOptions [
|
||||
{
|
||||
signature: 'app'
|
||||
parameter: 'appId'
|
||||
description: 'id of the application to preload'
|
||||
alias: 'a'
|
||||
}
|
||||
{
|
||||
signature: 'commit'
|
||||
parameter: 'hash'
|
||||
description: '''
|
||||
The commit hash for a specific application release to preload, use "current" to specify the current
|
||||
release (ignored if no appId is given). The current release is usually also the latest, but can be
|
||||
manually pinned using https://github.com/balena-io-projects/staged-releases .
|
||||
'''
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'splash-image'
|
||||
parameter: 'splashImage.png'
|
||||
description: 'path to a png image to replace the splash screen'
|
||||
alias: 's'
|
||||
}
|
||||
{
|
||||
signature: 'dont-check-arch'
|
||||
boolean: true
|
||||
description: 'Disables check for matching architecture in image and application'
|
||||
}
|
||||
{
|
||||
signature: 'pin-device-to-release'
|
||||
boolean: true
|
||||
description: 'Pin the preloaded device to the preloaded release on provision'
|
||||
alias: 'p'
|
||||
}
|
||||
{
|
||||
signature: 'add-certificate'
|
||||
parameter: 'certificate.crt'
|
||||
description: '''
|
||||
Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container.
|
||||
The file name must end with '.crt' and must not be already contained in the preloader's
|
||||
/etc/ssl/certs folder.
|
||||
Can be repeated to add multiple certificates.
|
||||
'''
|
||||
}
|
||||
]
|
||||
# Remove dockerPort `-p` alias as it conflicts with pin-device-to-release
|
||||
delete _.find(preloadOptions, signature: 'dockerPort').alias
|
||||
|
||||
module.exports =
|
||||
signature: 'preload <image>'
|
||||
description: '(beta) preload an app on a disk image (or Edison zip archive)'
|
||||
description: 'preload an app on a disk image (or Edison zip archive)'
|
||||
help: '''
|
||||
Warning: "resin preload" requires Docker to be correctly installed in
|
||||
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/resin-io/resin-cli .
|
||||
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 commit from Resin.io.
|
||||
This can be used with cloud builds, or images deployed with resin deploy.
|
||||
Edison zip archive) with a built release from balena.
|
||||
|
||||
Examples:
|
||||
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
|
||||
$ resin preload resin.img
|
||||
|
||||
$ balena preload balena.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image image.png
|
||||
$ balena preload balena.img
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: dockerUtils.appendConnectionOptions [
|
||||
{
|
||||
signature: 'app'
|
||||
parameter: 'appId'
|
||||
description: 'id of the application to preload'
|
||||
alias: 'a'
|
||||
}
|
||||
{
|
||||
signature: 'commit'
|
||||
parameter: 'hash'
|
||||
description: '''
|
||||
a specific application commit to preload, use "latest" to specify the latest commit
|
||||
(ignored if no appId is given)
|
||||
'''
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'splash-image'
|
||||
parameter: 'splashImage.png'
|
||||
description: 'path to a png image to replace the splash screen'
|
||||
alias: 's'
|
||||
}
|
||||
{
|
||||
signature: 'dont-check-device-type'
|
||||
boolean: true
|
||||
description: 'Disables check for matching device types in image and application'
|
||||
}
|
||||
]
|
||||
options: preloadOptions
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
form = require('resin-cli-form')
|
||||
preload = require('resin-preload')
|
||||
errors = require('resin-errors')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
preload = require('balena-preload')
|
||||
visuals = require('resin-cli-visuals')
|
||||
nodeCleanup = require('node-cleanup')
|
||||
{ expectedError } = require('../utils/patterns')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
progressBars = {}
|
||||
|
||||
@ -178,6 +229,7 @@ module.exports =
|
||||
console.log()
|
||||
spinner.stop()
|
||||
|
||||
options.commit = if isCurrent(options.commit) then 'latest' else options.commit
|
||||
options.image = params.image
|
||||
options.appId = options.app
|
||||
delete options.app
|
||||
@ -185,21 +237,39 @@ module.exports =
|
||||
options.splashImage = options['splash-image']
|
||||
delete options['splash-image']
|
||||
|
||||
if options['dont-check-device-type'] and not options.appId
|
||||
expectedError('You need to specify an app id if you disable the device type check.')
|
||||
options.dontCheckArch = options['dont-check-arch'] || false
|
||||
delete options['dont-check-arch']
|
||||
if options.dontCheckArch and not options.appId
|
||||
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
|
||||
|
||||
options.pinDevice = options['pin-device-to-release'] || false
|
||||
delete options['pin-device-to-release']
|
||||
|
||||
if _.isArray(options['add-certificate'])
|
||||
certificates = options['add-certificate']
|
||||
else if options['add-certificate'] == undefined
|
||||
certificates = []
|
||||
else
|
||||
certificates = [ options['add-certificate'] ]
|
||||
for certificate in certificates
|
||||
if not certificate.endsWith('.crt')
|
||||
exitWithExpectedError('Certificate file name must end with ".crt"')
|
||||
|
||||
# Get a configured dockerode instance
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
|
||||
preloader = new preload.Preloader(
|
||||
resin,
|
||||
docker,
|
||||
options.appId,
|
||||
options.commit,
|
||||
options.image,
|
||||
options.splashImage,
|
||||
options.proxy,
|
||||
balena
|
||||
docker
|
||||
options.appId
|
||||
options.commit
|
||||
options.image
|
||||
options.splashImage
|
||||
options.proxy
|
||||
options.dontCheckArch
|
||||
options.pinDevice
|
||||
certificates
|
||||
)
|
||||
|
||||
gotSignal = false
|
||||
@ -223,51 +293,39 @@ module.exports =
|
||||
return new Promise (resolve, reject) ->
|
||||
preloader.on('error', reject)
|
||||
|
||||
preloader.build()
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.prepare()
|
||||
.then ->
|
||||
preloader.getDeviceTypeAndPreloadedBuilds()
|
||||
.then (info) ->
|
||||
# If no appId was provided, show a list of matching apps
|
||||
Promise.try ->
|
||||
if options.appId
|
||||
return preloader.fetchApplication()
|
||||
.catch(errors.ResinApplicationNotFound, expectedError)
|
||||
selectApplication(info.device_type)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
# Check that the app device type and the image device type match
|
||||
if not options['dont-check-device-type'] and info.device_type != application.device_type
|
||||
expectedError(
|
||||
"Image device type (#{info.device_type}) and application device type (#{application.device_type}) do not match"
|
||||
)
|
||||
if not preloader.appId
|
||||
selectApplication(preloader.config.deviceType)
|
||||
.then (application) ->
|
||||
preloader.setApplication(application)
|
||||
.then ->
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if isCurrent(options.commit) and preloader.application.commit
|
||||
# handle `--commit current` (and its `--commit latest` synonym)
|
||||
return 'latest'
|
||||
release = _.find preloader.application.owns__release, (release) ->
|
||||
release.commit.startsWith(options.commit)
|
||||
if not release
|
||||
exitWithExpectedError('There is no release matching this commit')
|
||||
return release.commit
|
||||
selectApplicationCommit(preloader.application.owns__release)
|
||||
.then (commit) ->
|
||||
if isCurrent(commit)
|
||||
preloader.commit = preloader.application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Use the commit given as --commit or show an interactive commit selection menu
|
||||
Promise.try ->
|
||||
if options.commit
|
||||
if options.commit == LATEST and application.commit
|
||||
# handle `--commit latest`
|
||||
return LATEST
|
||||
else if not _.find(application.build, commit_hash: options.commit)
|
||||
expectedError('There is no build matching this commit')
|
||||
return options.commit
|
||||
selectApplicationCommit(application.build)
|
||||
.then (commit) ->
|
||||
if commit == LATEST
|
||||
preloader.commit = application.commit
|
||||
else
|
||||
preloader.commit = commit
|
||||
|
||||
# Propose to disable automatic app updates if the commit is not the latest
|
||||
offerToDisableAutomaticUpdates(application, commit)
|
||||
.then ->
|
||||
builds = info.preloaded_builds.map (build) ->
|
||||
build.slice(-preload.BUILD_HASH_LENGTH)
|
||||
if preloader.commit in builds
|
||||
throw new preload.errors.ResinError('This build is already preloaded in this image.')
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(preload.errors.ResinError, expectedError)
|
||||
# Propose to disable automatic app updates if the commit is not the current release
|
||||
offerToDisableAutomaticUpdates(preloader.application, commit, options.pinDevice)
|
||||
.then ->
|
||||
# All options are ready: preload the image.
|
||||
preloader.preload()
|
||||
.catch(balena.errors.BalenaError, exitWithExpectedError)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.then(done)
|
||||
|
380
lib/actions/push.ts
Normal file
380
lib/actions/push.ts
Normal file
@ -0,0 +1,380 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { registrySecretsHelp } from '../utils/messages';
|
||||
import {
|
||||
validateApplicationName,
|
||||
validateDotLocalUrl,
|
||||
validateIPAddress,
|
||||
} from '../utils/validation';
|
||||
|
||||
enum BuildTarget {
|
||||
Cloud,
|
||||
Device,
|
||||
}
|
||||
|
||||
function getBuildTarget(appOrDevice: string): BuildTarget | null {
|
||||
// First try the application regex from the api
|
||||
if (validateApplicationName(appOrDevice)) {
|
||||
return BuildTarget.Cloud;
|
||||
}
|
||||
|
||||
if (validateIPAddress(appOrDevice) || validateDotLocalUrl(appOrDevice)) {
|
||||
return BuildTarget.Device;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getAppOwner(sdk: BalenaSDK, appName: string) {
|
||||
const { exitWithExpectedError, selectFromList } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const _ = await import('lodash');
|
||||
|
||||
const applications = await sdk.models.application.getAll({
|
||||
$expand: {
|
||||
user: {
|
||||
$select: ['username'],
|
||||
},
|
||||
},
|
||||
$filter: {
|
||||
$eq: [{ $tolower: { $: 'app_name' } }, appName.toLowerCase()],
|
||||
},
|
||||
$select: ['id'],
|
||||
});
|
||||
|
||||
if (applications == null || applications.length === 0) {
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
No applications found with name: ${appName}.
|
||||
|
||||
This could mean that the application does not exist, or you do
|
||||
not have the permissions required to access it.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (applications.length === 1) {
|
||||
return _.get(applications, '[0].user[0].username');
|
||||
}
|
||||
|
||||
// If we got more than one application with the same name it means that the
|
||||
// user has access to a collab app with the same name as a personal app. We
|
||||
// present a list to the user which shows the fully qualified application
|
||||
// name (user/appname) and allows them to select
|
||||
const entries = _.map(applications, app => {
|
||||
const username = _.get(app, 'user[0].username');
|
||||
return {
|
||||
name: `${username}/${appName}`,
|
||||
extra: username,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await selectFromList(
|
||||
`${
|
||||
entries.length
|
||||
} applications found with that name, please select the application you would like to push to`,
|
||||
entries,
|
||||
);
|
||||
|
||||
return selected.extra;
|
||||
}
|
||||
|
||||
export const push: CommandDefinition<
|
||||
{
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
applicationOrDevice: string;
|
||||
applicationOrDevice_raw: string;
|
||||
},
|
||||
{
|
||||
source?: string;
|
||||
emulated?: boolean;
|
||||
dockerfile?: string; // DeviceDeployOptions.dockerfilePath (alternative Dockerfile)
|
||||
nocache?: boolean;
|
||||
'registry-secrets'?: string;
|
||||
nolive?: boolean;
|
||||
detached?: boolean;
|
||||
service?: string | string[];
|
||||
system?: boolean;
|
||||
env?: string | string[];
|
||||
}
|
||||
> = {
|
||||
signature: 'push <applicationOrDevice>',
|
||||
primary: true,
|
||||
description:
|
||||
'Start a remote build on the balena cloud build servers or a local mode device',
|
||||
help: stripIndent`
|
||||
This command can be used to start a build on the remote balena cloud builders,
|
||||
or a local mode balena device.
|
||||
|
||||
When building on the balenaCloud servers, the given source directory will be
|
||||
sent to the remote server. This can be used as a drop-in replacement for the
|
||||
"git push" deployment method.
|
||||
|
||||
When building on a local mode device, the given source directory will be
|
||||
built on the device, and the resulting containers will be run on the device.
|
||||
Logs will be streamed back from the device as part of the same invocation.
|
||||
The web dashboard can be used to switch a device to local mode:
|
||||
https://www.balena.io/docs/learn/develop/local-mode/
|
||||
Note that local mode requires a supervisor version of at least v7.21.0.
|
||||
The logs from only a single service can be shown with the --service flag, and
|
||||
showing only the system logs can be achieved with --system. Note that these
|
||||
flags can be used together.
|
||||
|
||||
When pushing to a local device a live session will be started.
|
||||
The project source folder is watched for filesystem events, and changes
|
||||
to files and folders are automatically synchronized to the running
|
||||
containers. The synchronisation is only in one direction, from this machine to
|
||||
the device, and changes made on the device itself may be overwritten.
|
||||
This feature requires a device running supervisor version v9.7.0 or greater.
|
||||
|
||||
${registrySecretsHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena push myApp
|
||||
$ balena push myApp --source <source directory>
|
||||
$ balena push myApp -s <source directory>
|
||||
|
||||
$ balena push 10.0.0.1
|
||||
$ balena push 10.0.0.1 --source <source directory>
|
||||
$ balena push 10.0.0.1 --service my-service
|
||||
$ balena push 10.0.0.1 --env MY_ENV_VAR=value --env my-service:SERVICE_VAR=value
|
||||
$ balena push 10.0.0.1 --nolive
|
||||
|
||||
$ balena push 23c73a1.local --system
|
||||
$ balena push 23c73a1.local --system --service my-service
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'source',
|
||||
alias: 's',
|
||||
description:
|
||||
'The source that should be sent to the balena builder to be built (defaults to the current directory)',
|
||||
parameter: 'source',
|
||||
},
|
||||
{
|
||||
signature: 'emulated',
|
||||
alias: 'e',
|
||||
description: 'Force an emulated build to occur on the remote builder',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'dockerfile',
|
||||
parameter: 'Dockerfile',
|
||||
description:
|
||||
'Alternative Dockerfile name/path, relative to the source folder',
|
||||
},
|
||||
{
|
||||
signature: 'nocache',
|
||||
alias: 'c',
|
||||
description: "Don't use cache when building this project",
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'registry-secrets',
|
||||
alias: 'R',
|
||||
parameter: 'secrets.yml|.json',
|
||||
description: stripIndent`
|
||||
Path to a local YAML or JSON file containing Docker registry passwords used to pull base images.
|
||||
Note that if registry-secrets are not provided on the command line, a secrets configuration
|
||||
file from the balena directory will be used (usually $HOME/.balena/secrets.yml|.json)`,
|
||||
},
|
||||
{
|
||||
signature: 'nolive',
|
||||
boolean: true,
|
||||
description: stripIndent`
|
||||
Don't run a live session on this push. The filesystem will not be monitored, and changes
|
||||
will not be synchronised to any running containers. Note that both this flag and --detached
|
||||
and required to cause the process to end once the initial build has completed.`,
|
||||
},
|
||||
{
|
||||
signature: 'detached',
|
||||
alias: 'd',
|
||||
description: `Don't tail application logs when pushing to a local mode device`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'service',
|
||||
description: stripIndent`
|
||||
Reject logs not originating from this service.
|
||||
This can be used in combination with --system and other --service flags.
|
||||
Only valid when pushing to a local mode device.`,
|
||||
parameter: 'service',
|
||||
},
|
||||
{
|
||||
signature: 'system',
|
||||
description: stripIndent`
|
||||
Only show system logs. This can be used in combination with --service.
|
||||
Only valid when pushing to a local mode device.`,
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'env',
|
||||
parameter: 'env',
|
||||
description: stripIndent`
|
||||
When performing a push to device, run the built containers with environment
|
||||
variables provided with this argument. Environment variables can be applied
|
||||
to individual services by adding their service name before the argument,
|
||||
separated by a colon, e.g:
|
||||
--env main:MY_ENV=value
|
||||
Note that if the service name cannot be found in the composition, the entire
|
||||
left hand side of the = character will be treated as the variable name.
|
||||
`,
|
||||
},
|
||||
],
|
||||
async action(params, options, done) {
|
||||
const sdk = (await import('balena-sdk')).fromSharedOptions();
|
||||
const Bluebird = await import('bluebird');
|
||||
const isArray = await import('lodash/isArray');
|
||||
const remote = await import('../utils/remote-build');
|
||||
const deviceDeploy = await import('../utils/device/deploy');
|
||||
const { exitIfNotLoggedIn, exitWithExpectedError } = await import(
|
||||
'../utils/patterns'
|
||||
);
|
||||
const { validateSpecifiedDockerfile, getRegistrySecrets } = await import(
|
||||
'../utils/compose_ts'
|
||||
);
|
||||
const { BuildError } = await import('../utils/device/errors');
|
||||
|
||||
const appOrDevice: string | null =
|
||||
params.applicationOrDevice_raw || params.applicationOrDevice;
|
||||
if (appOrDevice == null) {
|
||||
exitWithExpectedError('You must specify an application or a device');
|
||||
}
|
||||
|
||||
const source = options.source || '.';
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[debug] Using ${source} as build source`);
|
||||
}
|
||||
|
||||
const dockerfilePath = validateSpecifiedDockerfile(
|
||||
source,
|
||||
options.dockerfile,
|
||||
);
|
||||
|
||||
const registrySecrets = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
|
||||
const buildTarget = getBuildTarget(appOrDevice);
|
||||
switch (buildTarget) {
|
||||
case BuildTarget.Cloud:
|
||||
// Ensure that the live argument has not been passed to a cloud build
|
||||
if (options.nolive != null) {
|
||||
exitWithExpectedError(
|
||||
'The --nolive flag is only valid when pushing to a local mode device',
|
||||
);
|
||||
}
|
||||
if (options.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.',
|
||||
);
|
||||
}
|
||||
if (options.env) {
|
||||
exitWithExpectedError(
|
||||
'The --env flag is only valid when pushing to a local mode device.',
|
||||
);
|
||||
}
|
||||
|
||||
const app = appOrDevice;
|
||||
await exitIfNotLoggedIn();
|
||||
await Bluebird.join(
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
getAppOwner(sdk, app),
|
||||
async (token, baseUrl, owner) => {
|
||||
const opts = {
|
||||
dockerfilePath,
|
||||
emulated: options.emulated || false,
|
||||
nocache: options.nocache || false,
|
||||
registrySecrets,
|
||||
};
|
||||
const args = {
|
||||
app,
|
||||
owner,
|
||||
source,
|
||||
auth: token,
|
||||
baseUrl,
|
||||
sdk,
|
||||
opts,
|
||||
};
|
||||
|
||||
return await remote.startRemoteBuild(args);
|
||||
},
|
||||
).nodeify(done);
|
||||
break;
|
||||
case BuildTarget.Device:
|
||||
const device = appOrDevice;
|
||||
const servicesToDisplay =
|
||||
options.service != null
|
||||
? isArray(options.service)
|
||||
? options.service
|
||||
: [options.service]
|
||||
: undefined;
|
||||
// TODO: Support passing a different port
|
||||
await Bluebird.resolve(
|
||||
deviceDeploy.deployToDevice({
|
||||
source,
|
||||
deviceHost: device,
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
nocache: options.nocache || false,
|
||||
nolive: options.nolive || false,
|
||||
detached: options.detached || false,
|
||||
services: servicesToDisplay,
|
||||
system: options.system || false,
|
||||
env:
|
||||
typeof options.env === 'string'
|
||||
? [options.env]
|
||||
: options.env || [],
|
||||
}),
|
||||
)
|
||||
.catch(BuildError, e => {
|
||||
exitWithExpectedError(e.toString());
|
||||
})
|
||||
.nodeify(done);
|
||||
break;
|
||||
default:
|
||||
exitWithExpectedError(
|
||||
stripIndent`
|
||||
Build target not recognised. Please provide either an application name or device address.
|
||||
|
||||
The only supported device addresses currently are IP addresses.
|
||||
|
||||
If you believe your build target should have been detected, and this is an error, please
|
||||
create an issue.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2017 Resin.io
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -33,15 +33,15 @@ dockerVersionProperties = [
|
||||
]
|
||||
|
||||
module.exports =
|
||||
signature: 'local scan'
|
||||
description: 'Scan for resinOS devices in your local network'
|
||||
signature: 'scan'
|
||||
description: 'Scan for balenaOS devices in your local network'
|
||||
help: '''
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin local scan
|
||||
$ resin local scan --timeout 120
|
||||
$ resin local scan --verbose
|
||||
$ balena scan
|
||||
$ balena scan --timeout 120
|
||||
$ balena scan --verbose
|
||||
'''
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
@ -60,30 +60,31 @@ module.exports =
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
prettyjson = require('prettyjson')
|
||||
Docker = require('docker-toolbelt')
|
||||
{ discover } = require('resin-sync')
|
||||
{ discover } = require('balena-sync')
|
||||
{ SpinnerPromise } = require('resin-cli-visuals')
|
||||
{ dockerPort, dockerTimeout } = require('./common')
|
||||
{ dockerPort, dockerTimeout } = require('./local/common')
|
||||
dockerUtils = require('../utils/docker')
|
||||
{ exitWithExpectedError } = require('../utils/patterns')
|
||||
|
||||
if options.timeout?
|
||||
options.timeout *= 1000
|
||||
|
||||
Promise.try ->
|
||||
new SpinnerPromise
|
||||
promise: discover.discoverLocalResinOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local resinOS devices..'
|
||||
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
|
||||
startMessage: 'Scanning for local balenaOS devices..'
|
||||
stopMessage: 'Reporting scan results'
|
||||
.filter ({ address }) ->
|
||||
Promise.try ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker.pingAsync()
|
||||
.return(true)
|
||||
.catchReturn(false)
|
||||
.tap (devices) ->
|
||||
if _.isEmpty(devices)
|
||||
throw new Error('Could not find any resinOS devices in the local network')
|
||||
exitWithExpectedError('Could not find any balenaOS devices in the local network')
|
||||
.map ({ host, address }) ->
|
||||
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
|
||||
Promise.props
|
||||
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
|
||||
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -12,23 +12,28 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
*/
|
||||
|
||||
exports.list =
|
||||
signature: 'settings'
|
||||
description: 'print current settings'
|
||||
help: '''
|
||||
Use this command to display detected settings
|
||||
import { CommandDefinition } from 'capitano';
|
||||
|
||||
Examples:
|
||||
export const list: CommandDefinition = {
|
||||
signature: 'settings',
|
||||
description: 'print current settings',
|
||||
help: `\
|
||||
Use this command to display detected settings
|
||||
|
||||
$ resin settings
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
prettyjson = require('prettyjson')
|
||||
Examples:
|
||||
|
||||
resin.settings.getAll()
|
||||
$ balena settings\
|
||||
`,
|
||||
async action(_params, _options, done) {
|
||||
const balena = (await import('balena-sdk')).fromSharedOptions();
|
||||
const prettyjson = await import('prettyjson');
|
||||
|
||||
return balena.settings
|
||||
.getAll()
|
||||
.then(prettyjson.render)
|
||||
.then(console.log)
|
||||
.nodeify(done)
|
||||
.nodeify(done);
|
||||
},
|
||||
};
|
@ -1,130 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'ssh [uuid]'
|
||||
description: '(beta) get a shell into the running app container of a device'
|
||||
help: '''
|
||||
Warning: 'resin ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check the README here: https://github.com/resin-io/resin-cli
|
||||
|
||||
Use this command to get a shell into the running application container of
|
||||
your device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin ssh MyApp
|
||||
$ resin ssh 7cf02a6
|
||||
$ resin ssh 7cf02a6 --port 8080
|
||||
$ resin ssh 7cf02a6 -v
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'port'
|
||||
parameter: 'port'
|
||||
description: 'ssh gateway port'
|
||||
alias: 'p'
|
||||
,
|
||||
signature: 'verbose'
|
||||
boolean: true
|
||||
description: 'increase verbosity'
|
||||
alias: 'v'
|
||||
,
|
||||
signature: 'noproxy'
|
||||
boolean: true
|
||||
description: "don't use the proxy configuration for this connection.
|
||||
Only makes sense if you've configured proxy globally."
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
child_process = require('child_process')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
bash = require('bash')
|
||||
hasbin = require('hasbin')
|
||||
{ getSubShellCommand } = require('../utils/helpers')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
options.port ?= 22
|
||||
|
||||
verbose = if options.verbose then '-vvv' else ''
|
||||
|
||||
proxyConfig = global.PROXY_CONFIG
|
||||
useProxy = !!proxyConfig and not options.noproxy
|
||||
|
||||
getSshProxyCommand = (hasTunnelBin) ->
|
||||
return '' if not useProxy
|
||||
|
||||
if not hasTunnelBin
|
||||
console.warn('''
|
||||
Proxy is enabled but the `proxytunnel` binary cannot be found.
|
||||
Please install it if you want to route the `resin ssh` requests through the proxy.
|
||||
Alternatively you can pass `--noproxy` param to the `resin ssh` command to ignore the proxy config
|
||||
for the `ssh` requests.
|
||||
|
||||
Attemmpting the unproxied request for now.
|
||||
''')
|
||||
return ''
|
||||
|
||||
tunnelOptions =
|
||||
proxy: "#{proxyConfig.host}:#{proxyConfig.port}"
|
||||
dest: '%h:%p'
|
||||
{ proxyAuth } = proxyConfig
|
||||
if proxyAuth
|
||||
i = proxyAuth.indexOf(':')
|
||||
_.assign tunnelOptions,
|
||||
user: proxyAuth.substring(0, i)
|
||||
pass: proxyAuth.substring(i + 1)
|
||||
proxyCommand = "proxytunnel #{bash.args(tunnelOptions, '--', '=')}"
|
||||
return "-o #{bash.args({ ProxyCommand: proxyCommand }, '', '=')}"
|
||||
|
||||
Promise.try ->
|
||||
return false if not params.uuid
|
||||
return resin.models.device.has(params.uuid)
|
||||
.then (uuidExists) ->
|
||||
return params.uuid if uuidExists
|
||||
return patterns.inferOrSelectDevice()
|
||||
.then (uuid) ->
|
||||
console.info("Connecting to: #{uuid}")
|
||||
resin.models.device.get(uuid)
|
||||
.then (device) ->
|
||||
throw new Error('Device is not online') if not device.is_online
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
uuid: device.uuid
|
||||
# get full uuid
|
||||
containerId: resin.models.device.getApplicationInfo(device.uuid).get('containerId')
|
||||
proxyUrl: resin.settings.get('proxyUrl')
|
||||
|
||||
hasTunnelBin: if useProxy then hasbin('proxytunnel') else null
|
||||
.then ({ username, uuid, containerId, proxyUrl, hasTunnelBin }) ->
|
||||
throw new Error('Did not find running application container') if not containerId?
|
||||
Promise.try ->
|
||||
sshProxyCommand = getSshProxyCommand(hasTunnelBin)
|
||||
command = "ssh #{verbose} -t \
|
||||
-o LogLevel=ERROR \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
#{sshProxyCommand} \
|
||||
-p #{options.port} #{username}@ssh.#{proxyUrl} enter #{uuid} #{containerId}"
|
||||
|
||||
subShellCommand = getSubShellCommand(command)
|
||||
child_process.spawn subShellCommand.program, subShellCommand.args,
|
||||
stdio: 'inherit'
|
||||
.nodeify(done)
|
368
lib/actions/ssh.ts
Normal file
368
lib/actions/ssh.ts
Normal file
@ -0,0 +1,368 @@
|
||||
/*
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as BalenaSdk from 'balena-sdk';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
import { BalenaDeviceNotFound } from 'balena-errors';
|
||||
import { validateDotLocalUrl, validateIPAddress } from '../utils/validation';
|
||||
|
||||
async function getContainerId(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
uuid: string,
|
||||
serviceName: string,
|
||||
sshOpts: {
|
||||
port?: number;
|
||||
proxyCommand?: string;
|
||||
proxyUrl: string;
|
||||
username: string;
|
||||
},
|
||||
version?: string,
|
||||
id?: number,
|
||||
): Promise<string> {
|
||||
const semver = await import('resin-semver');
|
||||
|
||||
if (version == null || id == null) {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version'],
|
||||
});
|
||||
version = device.supervisor_version;
|
||||
id = device.id;
|
||||
}
|
||||
|
||||
let containerId: string | undefined;
|
||||
if (semver.gte(version, '8.6.0')) {
|
||||
const apiUrl = await sdk.settings.get('apiUrl');
|
||||
// TODO: Move this into the SDKs device model
|
||||
const request = await sdk.request.send({
|
||||
method: 'POST',
|
||||
url: '/supervisor/v2/containerId',
|
||||
baseUrl: apiUrl,
|
||||
body: {
|
||||
method: 'GET',
|
||||
deviceId: id,
|
||||
},
|
||||
});
|
||||
if (request.status !== 200) {
|
||||
throw new Error(
|
||||
`There was an error connecting to device ${uuid}, HTTP response code: ${
|
||||
request.status
|
||||
}.`,
|
||||
);
|
||||
}
|
||||
const body = request.body;
|
||||
if (body.status !== 'success') {
|
||||
throw new Error(
|
||||
`There was an error communicating with device ${uuid}.\n\tError: ${
|
||||
body.message
|
||||
}`,
|
||||
);
|
||||
}
|
||||
containerId = body.services[serviceName];
|
||||
} else {
|
||||
console.log(stripIndent`
|
||||
Using legacy method to detect container ID. This will be slow.
|
||||
To speed up this process, please update your device to an OS
|
||||
which has a supervisor version of at least v8.6.0.
|
||||
`);
|
||||
// We need to execute a balena ps command on the device,
|
||||
// and parse the output, looking for a specific
|
||||
// container
|
||||
const { child_process } = await import('mz');
|
||||
const escapeRegex = await import('lodash/escapeRegExp');
|
||||
const { getSubShellCommand } = await import('../utils/helpers');
|
||||
const { deviceContainerEngineBinary } = await import('../utils/device/ssh');
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
verbose: false,
|
||||
port: sshOpts.port,
|
||||
command: `host ${uuid} '"${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"'`,
|
||||
proxyCommand: sshOpts.proxyCommand,
|
||||
proxyUrl: sshOpts.proxyUrl,
|
||||
username: sshOpts.username,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
const subprocess = child_process.spawn(
|
||||
subShellCommand.program,
|
||||
subShellCommand.args,
|
||||
{
|
||||
stdio: [null, 'pipe', null],
|
||||
},
|
||||
);
|
||||
const containers = await new Promise<string>((resolve, reject) => {
|
||||
let output = '';
|
||||
subprocess.stdout.on('data', chunk => (output += chunk.toString()));
|
||||
subprocess.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
reject(
|
||||
new Error(
|
||||
`Non-zero error code when looking for service container: ${code}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
resolve(output);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const lines = containers.split('\n');
|
||||
const regex = new RegExp(`\\/?${escapeRegex(serviceName)}_\\d+_\\d+`);
|
||||
for (const container of lines) {
|
||||
const [cId, name] = container.split(' ');
|
||||
if (regex.test(name)) {
|
||||
containerId = cId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
throw new Error(
|
||||
`Could not find a service ${serviceName} on device ${uuid}.`,
|
||||
);
|
||||
}
|
||||
return containerId;
|
||||
}
|
||||
|
||||
function generateVpnSshCommand(opts: {
|
||||
uuid: string;
|
||||
command: string;
|
||||
verbose: boolean;
|
||||
port?: number;
|
||||
username: string;
|
||||
proxyUrl: string;
|
||||
proxyCommand?: string;
|
||||
}) {
|
||||
return (
|
||||
`ssh ${
|
||||
opts.verbose ? '-vvv' : ''
|
||||
} -t -o LogLevel=ERROR -o StrictHostKeyChecking=no ` +
|
||||
`-o UserKnownHostsFile=/dev/null ` +
|
||||
`${opts.proxyCommand != null ? opts.proxyCommand : ''} ` +
|
||||
`${opts.port != null ? `-p ${opts.port}` : ''} ` +
|
||||
`${opts.username}@ssh.${opts.proxyUrl} ${opts.command}`
|
||||
);
|
||||
}
|
||||
|
||||
export const ssh: CommandDefinition<
|
||||
{
|
||||
applicationOrDevice: string;
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
applicationOrDevice_raw: string;
|
||||
serviceName?: string;
|
||||
},
|
||||
{
|
||||
port: string;
|
||||
service: string;
|
||||
verbose: true | undefined;
|
||||
noProxy: boolean;
|
||||
}
|
||||
> = {
|
||||
signature: 'ssh <applicationOrDevice> [serviceName]',
|
||||
description: 'SSH into the host or application container of a device',
|
||||
primary: true,
|
||||
help: stripIndent`
|
||||
This command can be used to start a shell on a local or remote device.
|
||||
|
||||
If a service name is not provided, a shell will be opened on the host OS.
|
||||
|
||||
If an application name is provided, all online devices in the application
|
||||
will be presented, and the chosen device will then have a shell opened on
|
||||
in it's service container or host OS.
|
||||
|
||||
For local devices, the ip address and .local domain name are supported.
|
||||
|
||||
Examples:
|
||||
balena ssh MyApp
|
||||
|
||||
balena ssh f49cefd
|
||||
balena ssh f49cefd my-service
|
||||
balena ssh f49cefd --port <port>
|
||||
|
||||
balena ssh 192.168.0.1 --verbose
|
||||
balena ssh f49cefd.local my-service
|
||||
|
||||
Warning: 'balena ssh' requires an openssh-compatible client to be correctly
|
||||
installed in your shell environment. For more information (including Windows
|
||||
support) please check:
|
||||
https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies`,
|
||||
options: [
|
||||
{
|
||||
signature: 'port',
|
||||
parameter: 'port',
|
||||
description: 'SSH gateway port',
|
||||
alias: 'p',
|
||||
},
|
||||
{
|
||||
signature: 'verbose',
|
||||
boolean: true,
|
||||
description: 'Increase verbosity',
|
||||
alias: 'v',
|
||||
},
|
||||
{
|
||||
signature: 'noproxy',
|
||||
boolean: true,
|
||||
description: stripIndent`
|
||||
Don't use the proxy configuration for this connection. This flag
|
||||
only make sense if you've configured a proxy globally.`,
|
||||
},
|
||||
],
|
||||
action: async (params, options) => {
|
||||
const applicationOrDevice =
|
||||
params.applicationOrDevice_raw || params.applicationOrDevice;
|
||||
const bash = await import('bash');
|
||||
// TODO: Make this typed
|
||||
const hasbin = require('hasbin');
|
||||
const { getSubShellCommand } = await import('../utils/helpers');
|
||||
const { child_process } = await import('mz');
|
||||
const {
|
||||
exitIfNotLoggedIn,
|
||||
exitWithExpectedError,
|
||||
getOnlineTargetUuid,
|
||||
} = await import('../utils/patterns');
|
||||
const sdk = BalenaSdk.fromSharedOptions();
|
||||
|
||||
const verbose = options.verbose === true;
|
||||
// ugh TODO: Fix this
|
||||
const proxyConfig = (global as any).PROXY_CONFIG;
|
||||
const useProxy = !!proxyConfig && !options.noProxy;
|
||||
const port = options.port != null ? parseInt(options.port, 10) : undefined;
|
||||
|
||||
// if we're doing a direct SSH connection locally...
|
||||
if (
|
||||
validateDotLocalUrl(applicationOrDevice) ||
|
||||
validateIPAddress(applicationOrDevice)
|
||||
) {
|
||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||
return await performLocalDeviceSSH({
|
||||
address: applicationOrDevice,
|
||||
port,
|
||||
verbose,
|
||||
service: params.serviceName,
|
||||
});
|
||||
}
|
||||
|
||||
// this will be a tunnelled SSH connection...
|
||||
exitIfNotLoggedIn();
|
||||
const uuid = await getOnlineTargetUuid(sdk, applicationOrDevice);
|
||||
let version: string | undefined;
|
||||
let id: number | undefined;
|
||||
|
||||
try {
|
||||
const device = await sdk.models.device.get(uuid, {
|
||||
$select: ['id', 'supervisor_version', 'is_online'],
|
||||
});
|
||||
id = device.id;
|
||||
version = device.supervisor_version;
|
||||
} catch (e) {
|
||||
if (e instanceof BalenaDeviceNotFound) {
|
||||
exitWithExpectedError(`Could not find device: ${uuid}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [hasTunnelBin, username, proxyUrl] = await Promise.all([
|
||||
useProxy ? await hasbin('proxytunnel') : undefined,
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('proxyUrl'),
|
||||
]);
|
||||
|
||||
const getSshProxyCommand = () => {
|
||||
if (!useProxy) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!hasTunnelBin) {
|
||||
console.warn(stripIndent`
|
||||
Proxy is enabled but the \`proxytunnel\` binary cannot be found.
|
||||
Please install it if you want to route the \`balena ssh\` requests through the proxy.
|
||||
Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config
|
||||
for the \`ssh\` requests.
|
||||
|
||||
Attempting the unproxied request for now.`);
|
||||
return '';
|
||||
}
|
||||
|
||||
let tunnelOptions: Dictionary<string> = {
|
||||
proxy: `${proxyConfig.host}:${proxyConfig.port}`,
|
||||
dest: '%h:%p',
|
||||
};
|
||||
const { proxyAuth } = proxyConfig;
|
||||
if (proxyAuth) {
|
||||
const i = proxyAuth.indexOf(':');
|
||||
tunnelOptions = {
|
||||
user: proxyAuth.substring(0, i),
|
||||
pass: proxyAuth.substring(i + 1),
|
||||
...tunnelOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const ProxyCommand = `proxytunnel ${bash.args(tunnelOptions, '--', '=')}`;
|
||||
return `-o ${bash.args({ ProxyCommand }, '', '=')}`;
|
||||
};
|
||||
|
||||
const proxyCommand = getSshProxyCommand();
|
||||
|
||||
if (username == null) {
|
||||
exitWithExpectedError(
|
||||
`Opening an SSH connection to a remote device requires you to be logged in.`,
|
||||
);
|
||||
}
|
||||
|
||||
// At this point, we have a long uuid with a device
|
||||
// that we know exists and is accessible
|
||||
let containerId: string | undefined;
|
||||
if (params.serviceName != null) {
|
||||
containerId = await getContainerId(
|
||||
sdk,
|
||||
uuid,
|
||||
params.serviceName,
|
||||
{
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
},
|
||||
version,
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
let accessCommand: string;
|
||||
if (containerId != null) {
|
||||
accessCommand = `enter ${uuid} ${containerId}`;
|
||||
} else {
|
||||
accessCommand = `host ${uuid}`;
|
||||
}
|
||||
|
||||
const command = generateVpnSshCommand({
|
||||
uuid,
|
||||
command: accessCommand,
|
||||
verbose,
|
||||
port,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
username: username!,
|
||||
});
|
||||
|
||||
const subShellCommand = getSubShellCommand(command);
|
||||
await child_process.spawn(subShellCommand.program, subShellCommand.args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
},
|
||||
};
|
277
lib/actions/tags.ts
Normal file
277
lib/actions/tags.ts
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
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);
|
||||
},
|
||||
};
|
235
lib/actions/tunnel.ts
Normal file
235
lib/actions/tunnel.ts
Normal file
@ -0,0 +1,235 @@
|
||||
/*
|
||||
Copyright 2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { createServer, Server, Socket } from 'net';
|
||||
import { isArray } from 'util';
|
||||
|
||||
import { getOnlineTargetUuid } from '../utils/patterns';
|
||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
||||
|
||||
interface Args {
|
||||
deviceOrApplication: string;
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
deviceOrApplication_raw: string;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
port: string | string[];
|
||||
}
|
||||
|
||||
class InvalidPortMappingError extends Error {
|
||||
constructor(mapping: string) {
|
||||
super(`'${mapping}' is not a valid port mapping.`);
|
||||
}
|
||||
}
|
||||
|
||||
class NoPortsDefinedError extends Error {
|
||||
constructor() {
|
||||
super('No ports have been provided.');
|
||||
}
|
||||
}
|
||||
|
||||
const isValidPort = (port: number) => {
|
||||
const MAX_PORT_VALUE = Math.pow(2, 16) - 1;
|
||||
return port > 0 && port <= MAX_PORT_VALUE;
|
||||
};
|
||||
|
||||
export const tunnel: CommandDefinition<Args, Options> = {
|
||||
signature: 'tunnel <deviceOrApplication>',
|
||||
description: 'Tunnel local ports to your balenaOS device',
|
||||
help: stripIndent`
|
||||
Use this command to open local ports which tunnel to listening ports on your balenaOS device.
|
||||
|
||||
For example, you could open port 8080 on your local machine to connect to your managed balenaOS
|
||||
device running a web server listening on port 3000.
|
||||
|
||||
You can tunnel multiple ports at any given time.
|
||||
|
||||
Examples:
|
||||
|
||||
# map remote port 22222 to localhost:22222
|
||||
$ balena tunnel abcde12345 -p 22222
|
||||
|
||||
# map remote port 22222 to localhost:222
|
||||
$ balena tunnel abcde12345 -p 22222:222
|
||||
|
||||
# map remote port 22222 to any address on your host machine, port 22222
|
||||
$ balena tunnel abcde12345 -p 22222:0.0.0.0
|
||||
|
||||
# map remote port 22222 to any address on your host machine, port 222
|
||||
$ balena tunnel abcde12345 -p 22222:0.0.0.0:222
|
||||
|
||||
# multiple port tunnels can be specified at any one time
|
||||
$ balena tunnel abcde12345 -p 8080:3000 -p 8081:9000
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'port',
|
||||
parameter: 'port',
|
||||
alias: 'p',
|
||||
description: 'The mapping of remote to local ports.',
|
||||
},
|
||||
],
|
||||
|
||||
primary: true,
|
||||
|
||||
action: async (params, options) => {
|
||||
const deviceOrApplication =
|
||||
params.deviceOrApplication_raw || params.deviceOrApplication;
|
||||
const Logger = await import('../utils/logger');
|
||||
const logger = 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);
|
||||
}
|
||||
};
|
||||
|
||||
if (options.port === undefined) {
|
||||
throw new NoPortsDefinedError();
|
||||
}
|
||||
|
||||
const ports =
|
||||
typeof options.port !== 'string' && isArray(options.port)
|
||||
? (options.port as string[])
|
||||
: [options.port as string];
|
||||
|
||||
const uuid = await getOnlineTargetUuid(sdk, deviceOrApplication);
|
||||
const device = await sdk.models.device.get(uuid);
|
||||
|
||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||
|
||||
const localListeners = _.chain(ports)
|
||||
.map(mapping => {
|
||||
const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec(
|
||||
mapping,
|
||||
);
|
||||
|
||||
if (regexResult === null) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// grab the groups
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let [, remotePort, localAddress, localPort] = regexResult;
|
||||
|
||||
if (
|
||||
!isValidPort(parseInt(localPort, undefined)) ||
|
||||
!isValidPort(parseInt(remotePort, undefined))
|
||||
) {
|
||||
throw new InvalidPortMappingError(mapping);
|
||||
}
|
||||
|
||||
// default bind to localAddress
|
||||
if (localAddress == null) {
|
||||
localAddress = 'localhost';
|
||||
}
|
||||
|
||||
// default use same port number locally as remote
|
||||
if (localPort == null) {
|
||||
localPort = remotePort;
|
||||
}
|
||||
|
||||
return {
|
||||
localPort: parseInt(localPort, undefined),
|
||||
localAddress,
|
||||
remotePort: parseInt(remotePort, undefined),
|
||||
};
|
||||
})
|
||||
.map(({ localPort, localAddress, remotePort }) => {
|
||||
return tunnelConnectionToDevice(device.uuid, remotePort, sdk)
|
||||
.then(handler =>
|
||||
createServer((client: Socket) => {
|
||||
return handler(client)
|
||||
.then(() => {
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
);
|
||||
})
|
||||
.catch(err =>
|
||||
logConnection(
|
||||
client.remoteAddress || '',
|
||||
client.remotePort || 0,
|
||||
client.localAddress,
|
||||
client.localPort,
|
||||
device.vpn_address || '',
|
||||
remotePort,
|
||||
err,
|
||||
),
|
||||
);
|
||||
}),
|
||||
)
|
||||
.then(
|
||||
server =>
|
||||
new Bluebird.Promise<Server>((resolve, reject) => {
|
||||
server.on('error', reject);
|
||||
server.listen(localPort, localAddress, () => {
|
||||
resolve(server);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
logger.logInfo(
|
||||
` - tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}`,
|
||||
);
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
logger.logWarn(
|
||||
` - not tunnelling ${localAddress}:${localPort} to ${
|
||||
device.uuid
|
||||
}:${remotePort}, failed ${JSON.stringify(err.message)}`,
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.value();
|
||||
|
||||
const results = await Promise.all(localListeners);
|
||||
if (!results.includes(true)) {
|
||||
throw new Error('No ports are valid for tunnelling');
|
||||
}
|
||||
|
||||
logger.logInfo('Waiting for connections...');
|
||||
},
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
_ = require('lodash')
|
||||
|
||||
exports.availableDrives =
|
||||
# TODO: dedupe with https://github.com/resin-io-modules/resin-cli-visuals/blob/master/lib/widgets/drive/index.coffee
|
||||
signature: 'util available-drives'
|
||||
description: 'list available drives'
|
||||
help: """
|
||||
Use this command to list your machine's drives usable for writing the OS image to.
|
||||
Skips the system drives.
|
||||
"""
|
||||
action: ->
|
||||
Promise = require('bluebird')
|
||||
drivelist = require('drivelist')
|
||||
driveListAsync = Promise.promisify(drivelist.list)
|
||||
chalk = require('chalk')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
formatDrive = (drive) ->
|
||||
size = drive.size / 1000000000
|
||||
return {
|
||||
device: drive.device
|
||||
size: "#{size.toFixed(1)} GB"
|
||||
description: drive.description
|
||||
}
|
||||
|
||||
getDrives = ->
|
||||
driveListAsync().then (drives) ->
|
||||
return _.reject(drives, system: true)
|
||||
|
||||
getDrives()
|
||||
.then (drives) ->
|
||||
if not drives.length
|
||||
console.error("#{chalk.red('x')} No available drives were detected, plug one in!")
|
||||
return
|
||||
|
||||
console.log visuals.table.horizontal drives.map(formatDrive), [
|
||||
'device'
|
||||
'size'
|
||||
'description'
|
||||
]
|
65
lib/actions/util.ts
Normal file
65
lib/actions/util.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
/*
|
||||
Copyright 2016-2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
|
||||
export const availableDrives: CommandDefinition<{}, {}> = {
|
||||
signature: 'util available-drives',
|
||||
description: 'list available drives',
|
||||
help: stripIndent`
|
||||
Use this command to list your machine's drives usable for writing the OS image to.
|
||||
Skips the system drives.
|
||||
`,
|
||||
async action() {
|
||||
const sdk = await import('etcher-sdk');
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
|
||||
function formatDrive(drive: any) {
|
||||
const size = drive.size / 1000000000;
|
||||
return {
|
||||
device: drive.device,
|
||||
size: `${size.toFixed(1)} GB`,
|
||||
description: drive.description,
|
||||
};
|
||||
}
|
||||
|
||||
if (scanner.drives.size === 0) {
|
||||
console.error(
|
||||
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
visuals.table.horizontal(Array.from(scanner.drives).map(formatDrive), [
|
||||
'device',
|
||||
'size',
|
||||
'description',
|
||||
]),
|
||||
);
|
||||
}
|
||||
scanner.stop();
|
||||
},
|
||||
};
|
@ -1,75 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.wizard =
|
||||
signature: 'quickstart [name]'
|
||||
description: 'getting started with resin.io'
|
||||
help: '''
|
||||
Use this command to run a friendly wizard to get started with resin.io.
|
||||
|
||||
The wizard will guide you through:
|
||||
|
||||
- Create an application.
|
||||
- Initialise an SDCard with the resin.io operating system.
|
||||
- Associate an existing project directory with your resin.io application.
|
||||
- Push your project to your devices.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin quickstart
|
||||
$ resin quickstart MyApp
|
||||
'''
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
console.info('Looks like you\'re not logged in yet!')
|
||||
console.info('Lets go through a quick wizard to get you started.\n')
|
||||
return capitanoRunAsync('login')
|
||||
.then ->
|
||||
return if params.name?
|
||||
patterns.selectOrCreateApplication().tap (applicationName) ->
|
||||
resin.models.application.has(applicationName).then (hasApplication) ->
|
||||
return applicationName if hasApplication
|
||||
capitanoRunAsync("app create #{applicationName}")
|
||||
.then (applicationName) ->
|
||||
params.name = applicationName
|
||||
.then ->
|
||||
return capitanoRunAsync("device init --application #{params.name}")
|
||||
.tap(patterns.awaitDevice)
|
||||
.then (uuid) ->
|
||||
return capitanoRunAsync("device #{uuid}")
|
||||
.then ->
|
||||
return resin.models.application.get(params.name)
|
||||
.then (application) ->
|
||||
console.log """
|
||||
Your device is ready to start pushing some code!
|
||||
|
||||
Check our official documentation for more information:
|
||||
|
||||
http://docs.resin.io/#/pages/introduction/introduction.md
|
||||
|
||||
Clone an example or go to an existing application directory and run:
|
||||
|
||||
$ git remote add resin #{application.git_repository}
|
||||
$ git push resin master
|
||||
"""
|
||||
.nodeify(done)
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
Copyright 2016-2019 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,87 +14,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
Raven = require('raven')
|
||||
Raven.disableConsoleAlerts()
|
||||
Raven.config require('./config').sentryDsn,
|
||||
captureUnhandledRejections: true
|
||||
release: require('../package.json').version
|
||||
.install (logged, error) ->
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
Raven.setContext
|
||||
extra:
|
||||
args: process.argv
|
||||
node_version: process.version
|
||||
|
||||
validNodeVersions = require('../package.json').engines.node
|
||||
if not require('semver').satisfies(process.version, validNodeVersions)
|
||||
console.warn """
|
||||
Warning: this version of Node does not match the requirements of this package.
|
||||
This package expects #{validNodeVersions}, but you're using #{process.version}.
|
||||
This may cause unexpected behaviour.
|
||||
|
||||
To upgrade your Node, visit https://nodejs.org/en/download/
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# Doing this before requiring any other modules,
|
||||
# including the 'resin-sdk', to prevent any module from reading the http proxy config
|
||||
# before us
|
||||
globalTunnel = require('global-tunnel-ng')
|
||||
settings = require('resin-settings-client')
|
||||
try
|
||||
proxy = settings.get('proxy') or null
|
||||
catch
|
||||
proxy = null
|
||||
# Init the tunnel even if the proxy is not configured
|
||||
# because it can also get the proxy from the http(s)_proxy env var
|
||||
# If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy)
|
||||
|
||||
# TODO: make this a feature of capitano https://github.com/resin-io/capitano/issues/48
|
||||
global.PROXY_CONFIG = globalTunnel.proxyConfig
|
||||
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
capitano = require('capitano')
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
|
||||
# We don't yet use resin-sdk directly everywhere, but we set up shared
|
||||
# options correctly so we can do safely in submodules
|
||||
require('resin-sdk').setSharedOptions(
|
||||
apiUrl: settings.get('apiUrl')
|
||||
imageMakerUrl: settings.get('imageMakerUrl')
|
||||
dataDirectory: settings.get('dataDirectory')
|
||||
retries: 2
|
||||
)
|
||||
# Keep using sdk-preconfigured for now, but only temporarily
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
actions = require('./actions')
|
||||
errors = require('./errors')
|
||||
events = require('./events')
|
||||
plugins = require('./utils/plugins')
|
||||
update = require('./utils/update')
|
||||
|
||||
# Assign bluebird as the global promise library
|
||||
# stream-to-promise will produce native promises if not
|
||||
# for this module, which could wreak havoc in this
|
||||
# bluebird-only codebase.
|
||||
require('any-promise/register/bluebird')
|
||||
|
||||
capitano.permission 'user', (done) ->
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
if not isLoggedIn
|
||||
throw new Error '''
|
||||
You have to log in to continue
|
||||
|
||||
Run the following command to go through the login wizard:
|
||||
|
||||
$ resin login
|
||||
'''
|
||||
.nodeify(done)
|
||||
require('./utils/patterns').exitIfNotLoggedIn()
|
||||
.then(done, done)
|
||||
|
||||
capitano.command
|
||||
signature: '*'
|
||||
@ -106,20 +33,16 @@ capitano.globalOption
|
||||
boolean: true
|
||||
alias: 'h'
|
||||
|
||||
# ---------- Info Module ----------
|
||||
capitano.command(actions.info.version)
|
||||
capitano.globalOption
|
||||
signature: 'version'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
|
||||
# ---------- Help Module ----------
|
||||
capitano.command(actions.help.help)
|
||||
|
||||
# ---------- Wizard Module ----------
|
||||
capitano.command(actions.wizard.wizard)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.signup)
|
||||
capitano.command(actions.auth.whoami)
|
||||
# ---------- Api key module ----------
|
||||
capitano.command(actions.apiKey.generate)
|
||||
|
||||
# ---------- App Module ----------
|
||||
capitano.command(actions.app.create)
|
||||
@ -128,6 +51,11 @@ capitano.command(actions.app.remove)
|
||||
capitano.command(actions.app.restart)
|
||||
capitano.command(actions.app.info)
|
||||
|
||||
# ---------- Auth Module ----------
|
||||
capitano.command(actions.auth.login)
|
||||
capitano.command(actions.auth.logout)
|
||||
capitano.command(actions.auth.whoami)
|
||||
|
||||
# ---------- Device Module ----------
|
||||
capitano.command(actions.device.list)
|
||||
capitano.command(actions.device.supported)
|
||||
@ -143,6 +71,7 @@ capitano.command(actions.device.getDeviceUrl)
|
||||
capitano.command(actions.device.hasDeviceUrl)
|
||||
capitano.command(actions.device.register)
|
||||
capitano.command(actions.device.move)
|
||||
capitano.command(actions.device.osUpdate)
|
||||
capitano.command(actions.device.info)
|
||||
|
||||
# ---------- Notes Module ----------
|
||||
@ -156,10 +85,14 @@ 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)
|
||||
@ -178,47 +111,48 @@ capitano.command(actions.config.generate)
|
||||
capitano.command(actions.settings.list)
|
||||
|
||||
# ---------- Logs Module ----------
|
||||
capitano.command(actions.logs)
|
||||
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)
|
||||
capitano.command(actions.ssh.ssh)
|
||||
|
||||
# ---------- Local ResinOS Module ----------
|
||||
# ---------- 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)
|
||||
capitano.command(actions.scan)
|
||||
|
||||
# ---------- 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)
|
||||
|
||||
update.notify()
|
||||
#------------ Push/remote builds -------
|
||||
capitano.command(actions.push.push)
|
||||
|
||||
plugins.register(/^resin-plugin-(.+)$/).then ->
|
||||
cli = capitano.parse(process.argv)
|
||||
#------------ Join/Leave -------
|
||||
capitano.command(actions.join.join)
|
||||
capitano.command(actions.leave.leave)
|
||||
|
||||
runCommand = ->
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
cli = capitano.parse(process.argv)
|
||||
runCommand = ->
|
||||
capitanoExecuteAsync = Promise.promisify(capitano.execute)
|
||||
if cli.global?.help
|
||||
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
|
||||
else
|
||||
capitanoExecuteAsync(cli)
|
||||
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
|
||||
.catch(errors.handle)
|
||||
Promise.all([events.trackCommand(cli), runCommand()])
|
||||
.catch(require('./errors').handleError)
|
107
lib/app-common.ts
Normal file
107
lib/app-common.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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
* @see https://docs.sentry.io/clients/node/
|
||||
*/
|
||||
function setupRaven() {
|
||||
const Raven = require('raven');
|
||||
Raven.disableConsoleAlerts();
|
||||
Raven.config(require('./config').sentryDsn, {
|
||||
captureUnhandledRejections: true,
|
||||
autoBreadcrumbs: true,
|
||||
release: require('../package.json').version,
|
||||
}).install(function(_logged: any, error: Error) {
|
||||
console.error(error);
|
||||
return process.exit(1);
|
||||
});
|
||||
|
||||
Raven.setContext({
|
||||
extra: {
|
||||
args: process.argv,
|
||||
node_version: process.version,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function checkNodeVersion() {
|
||||
const validNodeVersions = require('../package.json').engines.node;
|
||||
if (!require('semver').satisfies(process.version, validNodeVersions)) {
|
||||
const { stripIndent } = require('common-tags');
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${
|
||||
process.version
|
||||
}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behaviour. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function setupGlobalHttpProxy() {
|
||||
// Doing this before requiring any other modules,
|
||||
// including the 'balena-sdk', to prevent any module from reading the http proxy config
|
||||
// before us
|
||||
const globalTunnel = require('global-tunnel-ng');
|
||||
const settings = require('balena-settings-client');
|
||||
let proxy;
|
||||
try {
|
||||
proxy = settings.get('proxy') || null;
|
||||
} catch (error1) {
|
||||
proxy = null;
|
||||
}
|
||||
|
||||
// Init the tunnel even if the proxy is not configured
|
||||
// because it can also get the proxy from the http(s)_proxy env var
|
||||
// If that is not set as well the initialize will do nothing
|
||||
globalTunnel.initialize(proxy);
|
||||
|
||||
// TODO: make this a feature of capitano https://github.com/balena-io/capitano/issues/48
|
||||
(global as any).PROXY_CONFIG = globalTunnel.proxyConfig;
|
||||
}
|
||||
|
||||
function setupBalenaSdkSharedOptions() {
|
||||
// We don't yet use balena-sdk directly everywhere, but we set up shared
|
||||
// options correctly so we can do safely in submodules
|
||||
const BalenaSdk = require('balena-sdk');
|
||||
const settings = require('balena-settings-client');
|
||||
BalenaSdk.setSharedOptions({
|
||||
apiUrl: settings.get('apiUrl'),
|
||||
imageMakerUrl: settings.get('imageMakerUrl'),
|
||||
dataDirectory: settings.get('dataDirectory'),
|
||||
retries: 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function globalInit() {
|
||||
setupRaven();
|
||||
checkNodeVersion();
|
||||
setupGlobalHttpProxy();
|
||||
setupBalenaSdkSharedOptions();
|
||||
|
||||
// Assign bluebird as the global promise library.
|
||||
// stream-to-promise will produce native promises if not for this module,
|
||||
// which is likely to lead to errors as much of the CLI coffeescript code
|
||||
// expects bluebird promises.
|
||||
require('any-promise/register/bluebird');
|
||||
|
||||
// check for CLI updates once a day
|
||||
require('./utils/update').notify();
|
||||
}
|
48
lib/app-oclif.ts
Normal file
48
lib/app-oclif.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Main } from '@oclif/command';
|
||||
import { ExitError } from '@oclif/errors';
|
||||
|
||||
import { handleError } from './errors';
|
||||
|
||||
class CustomMain extends Main {
|
||||
protected _helpOverride(): boolean {
|
||||
// Disable oclif's default handler for the 'version' command
|
||||
if (['-v', '--version', 'version'].includes(this.argv[0])) {
|
||||
return false;
|
||||
} else {
|
||||
return super._helpOverride();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* oclif CLI entrypoint
|
||||
*/
|
||||
export function run(argv: string[]) {
|
||||
CustomMain.run(argv.slice(2)).then(
|
||||
require('@oclif/command/flush'),
|
||||
(error: Error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
if (error instanceof ExitError && error.oclif.exit === 0) {
|
||||
return;
|
||||
}
|
||||
handleError(error);
|
||||
},
|
||||
);
|
||||
}
|
158
lib/app.ts
Normal file
158
lib/app.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @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 { stripIndent } from 'common-tags';
|
||||
|
||||
import { exitWithExpectedError } from './utils/patterns';
|
||||
|
||||
/**
|
||||
* Simple command-line pre-parsing to choose between oclif or Capitano.
|
||||
* @param argv process.argv
|
||||
*/
|
||||
function routeCliFramework(argv: string[]): void {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(
|
||||
`Debug: original argv0="${process.argv0}" argv=[${argv}] length=${
|
||||
argv.length
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const cmdSlice = argv.slice(2);
|
||||
|
||||
// Look for commands that have been deleted, to print a notice
|
||||
checkDeletedCommand(cmdSlice);
|
||||
|
||||
if (cmdSlice.length > 0) {
|
||||
// convert 'balena --version' or 'balena -v' to 'balena version'
|
||||
if (['--version', '-v'].includes(cmdSlice[0])) {
|
||||
cmdSlice[0] = 'version';
|
||||
}
|
||||
// convert 'balena --help' or 'balena -h' to 'balena help'
|
||||
else if (['--help', '-h'].includes(cmdSlice[0])) {
|
||||
cmdSlice[0] = 'help';
|
||||
}
|
||||
// convert e.g. 'balena help env add' to 'balena env add --help'
|
||||
if (cmdSlice.length > 1 && cmdSlice[0] === 'help') {
|
||||
cmdSlice.shift();
|
||||
cmdSlice.push('--help');
|
||||
}
|
||||
}
|
||||
|
||||
const [isOclif, isTopic] = isOclifCommand(cmdSlice);
|
||||
if (isOclif) {
|
||||
if (isTopic) {
|
||||
// convert space-separated commands to oclif's topic:command syntax
|
||||
argv = [
|
||||
argv[0],
|
||||
argv[1],
|
||||
cmdSlice[0] + ':' + cmdSlice[1],
|
||||
...cmdSlice.slice(2),
|
||||
];
|
||||
} else {
|
||||
argv = [argv[0], argv[1], ...cmdSlice];
|
||||
}
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`Debug: new argv=[${argv}] length=${argv.length}`);
|
||||
}
|
||||
require('./app-oclif').run(argv);
|
||||
} else {
|
||||
require('./app-capitano');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param argvSlice process.argv.slice(2)
|
||||
*/
|
||||
function checkDeletedCommand(argvSlice: string[]): void {
|
||||
if (argvSlice[0] === 'help') {
|
||||
argvSlice = argvSlice.slice(1);
|
||||
}
|
||||
function replaced(
|
||||
oldCmd: string,
|
||||
alternative: string,
|
||||
version: string,
|
||||
verb = 'replaced',
|
||||
) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Note: the command "balena ${oldCmd}" was ${verb} in CLI version ${version}.
|
||||
Please use "balena ${alternative}" instead.
|
||||
`);
|
||||
}
|
||||
function removed(oldCmd: string, alternative: string, version: string) {
|
||||
let msg = `Note: the command "balena ${oldCmd}" was removed in CLI version ${version}.`;
|
||||
if (alternative) {
|
||||
msg = [msg, alternative].join('\n');
|
||||
}
|
||||
exitWithExpectedError(msg);
|
||||
}
|
||||
const stopAlternative =
|
||||
'Please use "balena ssh -s" to access the host OS, then use `balena-engine stop`.';
|
||||
const cmds: { [cmd: string]: [(...args: any) => void, ...string[]] } = {
|
||||
sync: [replaced, 'push', 'v11.0.0', 'removed'],
|
||||
'local logs': [replaced, 'logs', 'v11.0.0'],
|
||||
'local push': [replaced, 'push', 'v11.0.0'],
|
||||
'local scan': [replaced, 'scan', 'v11.0.0'],
|
||||
'local ssh': [replaced, 'ssh', 'v11.0.0'],
|
||||
'local stop': [removed, stopAlternative, 'v11.0.0'],
|
||||
};
|
||||
let cmd: string | undefined;
|
||||
if (argvSlice.length > 1) {
|
||||
cmd = [argvSlice[0], argvSlice[1]].join(' ');
|
||||
} else if (argvSlice.length > 0) {
|
||||
cmd = argvSlice[0];
|
||||
}
|
||||
if (cmd && Object.getOwnPropertyNames(cmds).includes(cmd)) {
|
||||
cmds[cmd][0](cmd, ...cmds[cmd].slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the CLI command has been converted from Capitano to oclif.
|
||||
* Return an array of two boolean values:
|
||||
* r[0] : whether the CLI command is implemented with oclif
|
||||
* r[1] : if r[0] is true, whether the CLI command is implemented with
|
||||
* oclif "topics" (colon-separated subcommands like `env:add`)
|
||||
* @param argvSlice process.argv.slice(2)
|
||||
*/
|
||||
function isOclifCommand(argvSlice: string[]): [boolean, boolean] {
|
||||
// Look for commands that have been transitioned to oclif
|
||||
if (argvSlice.length > 0) {
|
||||
// balena version
|
||||
if (argvSlice[0] === 'version') {
|
||||
return [true, false];
|
||||
}
|
||||
if (argvSlice.length > 1) {
|
||||
// balena env add
|
||||
if (argvSlice[0] === 'env' && argvSlice[1] === 'add') {
|
||||
return [true, true];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
|
||||
* call this function.
|
||||
*/
|
||||
export function run(): void {
|
||||
// globalInit() must be called very early on (before other imports) because
|
||||
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
|
||||
// shared options, and performs node version requirement checks.
|
||||
require('./app-common').globalInit();
|
||||
routeCliFramework(process.argv);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -18,19 +18,19 @@ limitations under the License.
|
||||
# @module auth
|
||||
###
|
||||
|
||||
open = require('open')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
open = require('opn')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
server = require('./server')
|
||||
utils = require('./utils')
|
||||
|
||||
###*
|
||||
# @summary Login to the Resin CLI using the web dashboard
|
||||
# @summary Login to the balena CLI using the web dashboard
|
||||
# @function
|
||||
# @public
|
||||
#
|
||||
# @description
|
||||
# This function opens the user's default browser and points it
|
||||
# to the Resin.io dashboard where the session token exchange will
|
||||
# to the balena dashboard where the session token exchange will
|
||||
# take place.
|
||||
#
|
||||
# Once the the token is retrieved, it's automatically persisted.
|
||||
@ -56,8 +56,8 @@ exports.login = ->
|
||||
# Leave a bit of time for the
|
||||
# server to get up and runing
|
||||
setTimeout ->
|
||||
open(loginUrl)
|
||||
open(loginUrl, { wait: false })
|
||||
, 1000
|
||||
|
||||
return server.awaitForToken(options)
|
||||
.tap(resin.auth.loginWithToken)
|
||||
.tap(balena.auth.loginWithToken)
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Resin CLI - Error</title>
|
||||
<title>Balena CLI - Error</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
@ -12,10 +12,10 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/sad.png" inline>
|
||||
<h1>Something went wrong</h1>
|
||||
<p>You couldn't login to the Resin CLI for some reason</p>
|
||||
<p>You couldn't login to the balena CLI for some reason</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://forums.resin.io/" class="button danger">Get help in our forums</a>
|
||||
<a href="https://forums.balena.io/" class="button danger">Get help in our forums</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Resin CLI - Success</title>
|
||||
<title>Balena CLI - Success</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" type="text/css" href="./static/style.css" inline>
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="center">
|
||||
<img class="icon" src="./static/images/happy.png" inline>
|
||||
<h1>Success!</h1>
|
||||
<p>You successfully logged in the Resin CLI</p>
|
||||
<p>You successfully logged in the balena CLI</p>
|
||||
<br>
|
||||
<br>
|
||||
<a href="<%= dashboardUrl %>" class="button normal">Go to the dashboard</a>
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -18,7 +18,7 @@ express = require('express')
|
||||
path = require('path')
|
||||
bodyParser = require('body-parser')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
utils = require('./utils')
|
||||
|
||||
createServer = ({ port, isDev } = {}) ->
|
||||
@ -77,9 +77,9 @@ exports.awaitForToken = (options) ->
|
||||
Promise.try ->
|
||||
if not token
|
||||
throw new Error('No token')
|
||||
return utils.isTokenValid(token)
|
||||
.tap (isValid) ->
|
||||
if not isValid
|
||||
return utils.loginIfTokenValid(token)
|
||||
.tap (loggedIn) ->
|
||||
if not loggedIn
|
||||
throw new Error('Invalid token')
|
||||
.then ->
|
||||
renderAndDone({ request, response, viewName: 'success', token })
|
||||
@ -96,6 +96,6 @@ exports.awaitForToken = (options) ->
|
||||
exports.getContext = getContext = (viewName) ->
|
||||
if viewName is 'success'
|
||||
return Promise.props
|
||||
dashboardUrl: resin.settings.get('dashboardUrl')
|
||||
dashboardUrl: balena.settings.get('dashboardUrl')
|
||||
|
||||
return Promise.resolve({})
|
||||
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016 Resin.io
|
||||
Copyright 2016 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
balena = require('balena-sdk').fromSharedOptions()
|
||||
_ = require('lodash')
|
||||
url = require('url')
|
||||
Promise = require('bluebird')
|
||||
@ -38,11 +38,11 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
# characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25')
|
||||
|
||||
resin.settings.get('dashboardUrl').then (dashboardUrl) ->
|
||||
balena.settings.get('dashboardUrl').then (dashboardUrl) ->
|
||||
return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}")
|
||||
|
||||
###*
|
||||
# @summary Check if a token is valid
|
||||
# @summary Log in using a token, but only if the token is valid
|
||||
# @function
|
||||
# @protected
|
||||
#
|
||||
@ -50,26 +50,31 @@ exports.getDashboardLoginURL = (callbackUrl) ->
|
||||
# This function checks that the token is not only well-structured
|
||||
# but that it also authenticates with the server successfully.
|
||||
#
|
||||
# @param {String} sessionToken - token
|
||||
# @fulfil {Boolean} - whether is valid or not
|
||||
# If authenticated, the token is persisted, if not then the previous
|
||||
# login state is restored.
|
||||
#
|
||||
# @param {String} token - session token or api key
|
||||
# @fulfil {Boolean} - whether the login was successful or not
|
||||
# @returns {Promise}
|
||||
#
|
||||
# utils.isTokenValid('...').then (isValid) ->
|
||||
# if isValid
|
||||
# utils.loginIfTokenValid('...').then (loggedIn) ->
|
||||
# if loggedIn
|
||||
# console.log('Token is valid!')
|
||||
###
|
||||
exports.isTokenValid = (sessionToken) ->
|
||||
if not sessionToken? or _.isEmpty(sessionToken.trim())
|
||||
exports.loginIfTokenValid = (token) ->
|
||||
if not token? or _.isEmpty(token.trim())
|
||||
return Promise.resolve(false)
|
||||
|
||||
return resin.token.get().then (currentToken) ->
|
||||
resin.auth.loginWithToken(sessionToken)
|
||||
.return(sessionToken)
|
||||
.then(resin.auth.isLoggedIn)
|
||||
return balena.auth.getToken()
|
||||
.catchReturn(undefined)
|
||||
.then (currentToken) ->
|
||||
balena.auth.loginWithToken(token)
|
||||
.return(token)
|
||||
.then(balena.auth.isLoggedIn)
|
||||
.tap (isLoggedIn) ->
|
||||
return if isLoggedIn
|
||||
|
||||
if currentToken?
|
||||
return resin.auth.loginWithToken(currentToken)
|
||||
return balena.auth.loginWithToken(currentToken)
|
||||
else
|
||||
return resin.auth.logout()
|
||||
return balena.auth.logout()
|
||||
|
@ -1 +1,2 @@
|
||||
exports.sentryDsn = 'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239'
|
||||
export const sentryDsn =
|
||||
'https://56d2a46124614b01b0f4086897e96110:6e175465accc41b595a96947155f61fb@sentry.io/149239';
|
||||
|
@ -1,38 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
chalk = require('chalk')
|
||||
errors = require('resin-cli-errors')
|
||||
patterns = require('./utils/patterns')
|
||||
Raven = require('raven')
|
||||
Promise = require('bluebird')
|
||||
|
||||
captureException = Promise.promisify(Raven.captureException.bind(Raven))
|
||||
|
||||
exports.handle = (error) ->
|
||||
message = errors.interpret(error)
|
||||
return if not message?
|
||||
|
||||
if process.env.DEBUG
|
||||
message = error.stack
|
||||
|
||||
patterns.printErrorMessage(message)
|
||||
|
||||
captureException(error)
|
||||
.timeout(1000)
|
||||
.catch(-> # Ignore any errors (from error logging, or timeouts)
|
||||
).finally ->
|
||||
process.exit(error.exitCode or 1)
|
125
lib/errors.ts
Normal file
125
lib/errors.ts
Normal file
@ -0,0 +1,125 @@
|
||||
/*
|
||||
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 Promise from 'bluebird';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as Raven from 'raven';
|
||||
|
||||
import * as patterns from './utils/patterns';
|
||||
|
||||
const captureException = Promise.promisify<string, Error>(
|
||||
Raven.captureException,
|
||||
{ context: Raven },
|
||||
);
|
||||
|
||||
function hasCode(error: any): error is Error & { code: string } {
|
||||
return error.code != null;
|
||||
}
|
||||
|
||||
function treatFailedBindingAsMissingModule(error: any): void {
|
||||
if (error.message.startsWith('Could not locate the bindings file.')) {
|
||||
error.code = 'MODULE_NOT_FOUND';
|
||||
}
|
||||
}
|
||||
|
||||
function interpret(error: any): string | undefined {
|
||||
if (!(error instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
treatFailedBindingAsMissingModule(error);
|
||||
|
||||
if (hasCode(error)) {
|
||||
const errorCodeHandler = messages[error.code];
|
||||
const message = errorCodeHandler && errorCodeHandler(error);
|
||||
|
||||
if (message) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(error.message)) {
|
||||
return `${error.code}: ${error.message}`;
|
||||
}
|
||||
} else {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
const messages: {
|
||||
[key: string]: (error: Error & { path?: string }) => string;
|
||||
} = {
|
||||
EISDIR: error => `File is a directory: ${error.path}`,
|
||||
|
||||
ENOENT: error => `No such file or directory: ${error.path}`,
|
||||
|
||||
ENOGIT: () => stripIndent`
|
||||
Git is not installed on this system.
|
||||
Head over to http://git-scm.com to install it and run this command again.`,
|
||||
|
||||
EPERM: () => stripIndent`
|
||||
You don't have enough privileges to run this operation.
|
||||
${
|
||||
os.platform() === 'win32'
|
||||
? 'Run a new Command Prompt as administrator and try running this command again.'
|
||||
: 'Try running this command again prefixing it with `sudo`.'
|
||||
}
|
||||
|
||||
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
|
||||
|
||||
EACCES: e => messages.EPERM(e),
|
||||
|
||||
ETIMEDOUT: () =>
|
||||
'Oops something went wrong, please check your connection and try again.',
|
||||
|
||||
MODULE_NOT_FOUND: () => stripIndent`
|
||||
Part of the CLI could not be loaded. This typically means your CLI install is in a broken state.
|
||||
${
|
||||
os.arch() === 'x64'
|
||||
? 'You can normally fix this by uninstalling and reinstalling the CLI.'
|
||||
: stripIndent`
|
||||
You're using an unsupported architecture (${os.arch()}), so this is typically caused by missing native modules.
|
||||
Reinstalling may help, but pay attention to errors in native module build steps en route.
|
||||
`
|
||||
}
|
||||
`,
|
||||
|
||||
BalenaExpiredToken: () => stripIndent`
|
||||
Looks like your session token is expired.
|
||||
Please try logging in again with:
|
||||
$ balena login`,
|
||||
};
|
||||
|
||||
export function handleError(error: any) {
|
||||
let message = interpret(error);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
message = error.stack;
|
||||
}
|
||||
|
||||
patterns.printErrorMessage(message!);
|
||||
|
||||
return captureException(error)
|
||||
.timeout(1000)
|
||||
.catch(function() {
|
||||
// Ignore any errors (from error logging, or timeouts)
|
||||
})
|
||||
.finally(() => process.exit(error.exitCode || 1));
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
_ = require('lodash')
|
||||
Mixpanel = require('mixpanel')
|
||||
Raven = require('raven')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
packageJSON = require('../package.json')
|
||||
|
||||
exports.getLoggerInstance = _.memoize ->
|
||||
return resin.models.config.getMixpanelToken().then(Mixpanel.init)
|
||||
|
||||
exports.trackCommand = (capitanoCommand) ->
|
||||
capitanoStateGetMatchCommandAsync = Promise.promisify(require('capitano').state.getMatchCommand)
|
||||
|
||||
return Promise.props
|
||||
resinUrl: resin.settings.get('resinUrl')
|
||||
username: resin.auth.whoami().catchReturn(undefined)
|
||||
mixpanel: exports.getLoggerInstance()
|
||||
.then ({ username, resinUrl, mixpanel }) ->
|
||||
return capitanoStateGetMatchCommandAsync(capitanoCommand.command).then (command) ->
|
||||
Raven.mergeContext(user: {
|
||||
id: username,
|
||||
username
|
||||
})
|
||||
mixpanel.track "[CLI] #{command.signature.toString()}",
|
||||
distinct_id: username
|
||||
argv: process.argv.join(' ')
|
||||
version: packageJSON.version
|
||||
node: process.version
|
||||
arch: process.arch
|
||||
resinUrl: resinUrl
|
||||
platform: process.platform
|
||||
command: capitanoCommand
|
||||
.timeout(100)
|
||||
.catchReturn()
|
67
lib/events.ts
Normal file
67
lib/events.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @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 BalenaSdk = require('balena-sdk');
|
||||
import Promise = require('bluebird');
|
||||
import * as Capitano from 'capitano';
|
||||
import _ = require('lodash');
|
||||
import Mixpanel = require('mixpanel');
|
||||
import Raven = require('raven');
|
||||
|
||||
import packageJSON = require('../package.json');
|
||||
|
||||
const getBalenaSdk = _.once(() => BalenaSdk.fromSharedOptions());
|
||||
const getMatchCommandAsync = Promise.promisify(Capitano.state.getMatchCommand);
|
||||
const getMixpanel = _.once<any>(() => {
|
||||
const settings = require('balena-settings-client');
|
||||
return Mixpanel.init('00000000000000000000000000000000', {
|
||||
host: `api.${settings.get('balenaUrl')}`,
|
||||
path: '/mixpanel',
|
||||
protocol: 'https',
|
||||
});
|
||||
});
|
||||
|
||||
export function trackCommand(capitanoCli: Capitano.Cli) {
|
||||
const balena = getBalenaSdk();
|
||||
return Promise.props({
|
||||
balenaUrl: balena.settings.get('balenaUrl'),
|
||||
username: balena.auth.whoami().catchReturn(undefined),
|
||||
mixpanel: getMixpanel(),
|
||||
})
|
||||
.then(({ username, balenaUrl, mixpanel }) => {
|
||||
return getMatchCommandAsync(capitanoCli.command).then(command => {
|
||||
Raven.mergeContext({
|
||||
user: {
|
||||
id: username,
|
||||
username,
|
||||
},
|
||||
});
|
||||
|
||||
return mixpanel.track(`[CLI] ${command.signature.toString()}`, {
|
||||
distinct_id: username,
|
||||
argv: process.argv.join(' '),
|
||||
version: packageJSON.version,
|
||||
node: process.version,
|
||||
arch: process.arch,
|
||||
balenaUrl,
|
||||
platform: process.platform,
|
||||
command: capitanoCli,
|
||||
});
|
||||
});
|
||||
})
|
||||
.timeout(100)
|
||||
.catchReturn(undefined);
|
||||
}
|
25
lib/global.d.ts
vendored
Normal file
25
lib/global.d.ts
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
declare module '*/package.json' {
|
||||
export const name: string;
|
||||
export const version: string;
|
||||
}
|
21
lib/utils/cloud.ts
Normal file
21
lib/utils/cloud.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import memoize = require('lodash/memoize');
|
||||
|
||||
export const serviceIdToName = memoize(
|
||||
async (sdk: BalenaSDK, serviceId: number): Promise<string | undefined> => {
|
||||
const serviceName = await sdk.pine.get({
|
||||
resource: 'service',
|
||||
id: serviceId,
|
||||
options: {
|
||||
$select: 'service_name',
|
||||
},
|
||||
});
|
||||
|
||||
if (serviceName != null) {
|
||||
return serviceName.service_name;
|
||||
}
|
||||
return;
|
||||
},
|
||||
// Memoize the call based on service id
|
||||
(_sdk, id) => id.toString(),
|
||||
);
|
814
lib/utils/compose.coffee
Normal file
814
lib/utils/compose.coffee
Normal file
@ -0,0 +1,814 @@
|
||||
###*
|
||||
# @license
|
||||
# Copyright 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.
|
||||
###
|
||||
|
||||
Promise = require('bluebird')
|
||||
path = require('path')
|
||||
|
||||
exports.appendProjectOptions = appendProjectOptions = (opts) ->
|
||||
opts.concat [
|
||||
{
|
||||
signature: 'projectName'
|
||||
parameter: 'projectName'
|
||||
description: 'Specify an alternate project name; default is the directory name'
|
||||
alias: 'n'
|
||||
},
|
||||
]
|
||||
|
||||
exports.appendOptions = (opts) ->
|
||||
appendProjectOptions(opts).concat [
|
||||
{
|
||||
signature: 'emulated'
|
||||
description: 'Run an emulated build using Qemu'
|
||||
boolean: true
|
||||
alias: 'e'
|
||||
},
|
||||
{
|
||||
signature: 'dockerfile'
|
||||
parameter: 'Dockerfile'
|
||||
description: 'Alternative Dockerfile name/path, relative to the source folder'
|
||||
},
|
||||
{
|
||||
signature: 'logs'
|
||||
description: 'Display full log output'
|
||||
boolean: true
|
||||
},
|
||||
{
|
||||
signature: 'registry-secrets'
|
||||
alias: 'R'
|
||||
parameter: 'secrets.yml|.json'
|
||||
description: 'Path to a YAML or JSON file with passwords for a private Docker registry'
|
||||
},
|
||||
]
|
||||
|
||||
exports.generateOpts = (options) ->
|
||||
fs = require('mz/fs')
|
||||
fs.realpath(options.source || '.').then (projectPath) ->
|
||||
projectName: options.projectName
|
||||
projectPath: projectPath
|
||||
inlineLogs: !!options.logs
|
||||
dockerfilePath: options.dockerfile
|
||||
|
||||
compositionFileNames = [
|
||||
'docker-compose.yml'
|
||||
'docker-compose.yaml'
|
||||
]
|
||||
|
||||
# look into the given directory for valid compose files and return
|
||||
# the contents of the first one found.
|
||||
exports.resolveProject = resolveProject = (rootDir) ->
|
||||
fs = require('mz/fs')
|
||||
Promise.any compositionFileNames.map (filename) ->
|
||||
fs.readFile(path.join(rootDir, filename), 'utf-8')
|
||||
|
||||
# Parse the given composition and return a structure with info. Input is:
|
||||
# - composePath: the *absolute* path to the directory containing the compose file
|
||||
# - composeStr: the contents of the compose file, as a string
|
||||
createProject = (composePath, composeStr, projectName = null) ->
|
||||
yml = require('js-yaml')
|
||||
compose = require('resin-compose-parse')
|
||||
|
||||
# both methods below may throw.
|
||||
composition = yml.safeLoad(composeStr, schema: yml.FAILSAFE_SCHEMA)
|
||||
composition = compose.normalize(composition)
|
||||
|
||||
projectName ?= path.basename(composePath)
|
||||
descriptors = compose.parse(composition).map (descr) ->
|
||||
# generate an image name based on the project and service names
|
||||
# if one is not given and the service requires a build
|
||||
if descr.image.context? and not descr.image.tag?
|
||||
descr.image.tag = [ projectName, descr.serviceName ].join('_').toLowerCase()
|
||||
return descr
|
||||
return {
|
||||
path: composePath,
|
||||
name: projectName,
|
||||
composition,
|
||||
descriptors
|
||||
}
|
||||
|
||||
# high-level function resolving a project and creating a composition out
|
||||
# of it in one go. if image is given, it'll create a default project for
|
||||
# that without looking for a project. falls back to creating a default
|
||||
# project if none is found at the given projectPath.
|
||||
exports.loadProject = (logger, projectPath, projectName, image, dockerfilePath) ->
|
||||
{ validateSpecifiedDockerfile } = require('./compose_ts')
|
||||
compose = require('resin-compose-parse')
|
||||
logger.logDebug('Loading project...')
|
||||
|
||||
Promise.try ->
|
||||
dockerfilePath = validateSpecifiedDockerfile(projectPath, dockerfilePath)
|
||||
|
||||
if image?
|
||||
logger.logInfo("Creating default composition with image: #{image}")
|
||||
return compose.defaultComposition(image)
|
||||
|
||||
logger.logDebug('Resolving project...')
|
||||
|
||||
resolveProject(projectPath)
|
||||
.tap ->
|
||||
if dockerfilePath
|
||||
logger.logWarn("Ignoring alternative dockerfile \"#{dockerfilePath}\"\ because a docker-compose file exists")
|
||||
else
|
||||
logger.logInfo('Compose file detected')
|
||||
.catch (e) ->
|
||||
logger.logDebug("Failed to resolve project: #{e}")
|
||||
logger.logInfo("Creating default composition with source: #{projectPath}")
|
||||
return compose.defaultComposition(undefined, dockerfilePath)
|
||||
.then (composeStr) ->
|
||||
logger.logDebug('Creating project...')
|
||||
createProject(projectPath, composeStr, projectName)
|
||||
|
||||
exports.tarDirectory = tarDirectory = (dir, preFinalizeCallback = null) ->
|
||||
tar = require('tar-stream')
|
||||
klaw = require('klaw')
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
{ FileIgnorer } = require('./ignore')
|
||||
{ toPosixPath } = require('resin-multibuild').PathUtils
|
||||
|
||||
getFiles = ->
|
||||
streamToPromise(klaw(dir))
|
||||
.filter((item) -> not item.stats.isDirectory())
|
||||
.map((item) -> item.path)
|
||||
|
||||
ignore = new FileIgnorer(dir)
|
||||
pack = tar.pack()
|
||||
getFiles(dir)
|
||||
.each (file) ->
|
||||
type = ignore.getIgnoreFileType(path.relative(dir, file))
|
||||
if type?
|
||||
ignore.addIgnoreFile(file, type)
|
||||
.filter(ignore.filter)
|
||||
.map (file) ->
|
||||
relPath = path.relative(path.resolve(dir), file)
|
||||
Promise.join relPath, fs.stat(file), fs.readFile(file),
|
||||
(filename, stats, data) ->
|
||||
pack.entry({ name: toPosixPath(filename), size: stats.size, mode: stats.mode }, data)
|
||||
.then ->
|
||||
preFinalizeCallback?(pack)
|
||||
.then ->
|
||||
pack.finalize()
|
||||
return pack
|
||||
|
||||
truncateString = (str, len) ->
|
||||
return str if str.length < len
|
||||
str = str.slice(0, len)
|
||||
# return everything up to the last line. this is a cheeky way to avoid
|
||||
# having to deal with splitting the string midway through some special
|
||||
# character sequence.
|
||||
return str.slice(0, str.lastIndexOf('\n'))
|
||||
|
||||
LOG_LENGTH_MAX = 512 * 1024 # 512KB
|
||||
|
||||
exports.buildProject = (
|
||||
docker, logger,
|
||||
projectPath, projectName, composition,
|
||||
arch, deviceType,
|
||||
emulated, buildOpts,
|
||||
inlineLogs
|
||||
) ->
|
||||
_ = require('lodash')
|
||||
humanize = require('humanize')
|
||||
compose = require('resin-compose-parse')
|
||||
builder = require('resin-multibuild')
|
||||
transpose = require('docker-qemu-transpose')
|
||||
qemu = require('./qemu')
|
||||
{ toPosixPath } = builder.PathUtils
|
||||
|
||||
logger.logInfo("Building for #{arch}/#{deviceType}")
|
||||
|
||||
imageDescriptors = compose.parse(composition)
|
||||
imageDescriptorsByServiceName = _.keyBy(imageDescriptors, 'serviceName')
|
||||
|
||||
if inlineLogs
|
||||
renderer = new BuildProgressInline(logger.streams['build'], imageDescriptors)
|
||||
else
|
||||
tty = require('./tty')(process.stdout)
|
||||
renderer = new BuildProgressUI(tty, imageDescriptors)
|
||||
renderer.start()
|
||||
|
||||
qemu.installQemuIfNeeded(emulated, logger, arch)
|
||||
.tap (needsQemu) ->
|
||||
return if not needsQemu
|
||||
logger.logInfo('Emulation is enabled')
|
||||
# Copy qemu into all build contexts
|
||||
Promise.map imageDescriptors, (d) ->
|
||||
return if not d.image.context? # external image
|
||||
return qemu.copyQemu(path.join(projectPath, d.image.context), arch)
|
||||
.then (needsQemu) ->
|
||||
# Tar up the directory, ready for the build stream
|
||||
tarDirectory(projectPath)
|
||||
.then (tarStream) ->
|
||||
{ makeBuildTasks } = require('./compose_ts')
|
||||
Promise.resolve(makeBuildTasks(composition, tarStream, { arch, deviceType }, logger))
|
||||
.map (task) ->
|
||||
d = imageDescriptorsByServiceName[task.serviceName]
|
||||
|
||||
# multibuild parses the composition internally so any tags we've
|
||||
# set before are lost; re-assign them here
|
||||
task.tag ?= [ projectName, task.serviceName ].join('_').toLowerCase()
|
||||
if d.image.context?
|
||||
d.image.tag = task.tag
|
||||
|
||||
# configure build opts appropriately
|
||||
task.dockerOpts ?= {}
|
||||
_.merge(task.dockerOpts, buildOpts, { t: task.tag })
|
||||
if d.image.context?.args?
|
||||
task.dockerOpts.buildargs ?= {}
|
||||
_.merge(task.dockerOpts.buildargs, d.image.context.args)
|
||||
|
||||
# Get the service-specific log stream
|
||||
# Caveat: `multibuild.BuildTask` defines no `logStream` property
|
||||
# but it's convenient to store it there; it's JS ultimately.
|
||||
task.logStream = renderer.streams[task.serviceName]
|
||||
task.logBuffer = []
|
||||
|
||||
# Setup emulation if needed
|
||||
return [ task, null ] if task.external or not needsQemu
|
||||
binPath = qemu.qemuPathInContext(path.join(projectPath, task.context))
|
||||
transpose.transposeTarStream task.buildStream,
|
||||
hostQemuPath: toPosixPath(binPath)
|
||||
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
|
||||
.then (stream) ->
|
||||
task.buildStream = stream
|
||||
.return([ task, binPath ])
|
||||
.map ([ task, qemuPath ]) ->
|
||||
Promise.resolve(task).tap (task) ->
|
||||
captureStream = buildLogCapture(task.external, task.logBuffer)
|
||||
|
||||
if task.external
|
||||
# External image -- there's no build to be performed,
|
||||
# just follow pull progress.
|
||||
captureStream.pipe(task.logStream)
|
||||
task.progressHook = pullProgressAdapter(captureStream)
|
||||
else
|
||||
task.streamHook = (stream) ->
|
||||
stream = createLogStream(stream)
|
||||
if qemuPath?
|
||||
buildThroughStream = transpose.getBuildThroughStream
|
||||
hostQemuPath: toPosixPath(qemuPath)
|
||||
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
|
||||
rawStream = stream.pipe(buildThroughStream)
|
||||
else
|
||||
rawStream = stream
|
||||
# `stream` sends out raw strings in contrast to `task.progressHook`
|
||||
# where we're given objects. capture these strings as they come
|
||||
# before we parse them.
|
||||
rawStream
|
||||
.pipe(dropEmptyLinesStream())
|
||||
.pipe(captureStream)
|
||||
.pipe(buildProgressAdapter(inlineLogs))
|
||||
.pipe(task.logStream)
|
||||
.then (tasks) ->
|
||||
logger.logDebug 'Prepared tasks; building...'
|
||||
builder.performBuilds(tasks, docker)
|
||||
.map (builtImage) ->
|
||||
if not builtImage.successful
|
||||
builtImage.error.serviceName = builtImage.serviceName
|
||||
throw builtImage.error
|
||||
|
||||
d = imageDescriptorsByServiceName[builtImage.serviceName]
|
||||
task = _.find(tasks, serviceName: builtImage.serviceName)
|
||||
|
||||
image =
|
||||
serviceName: d.serviceName
|
||||
name: d.image.tag ? d.image
|
||||
logs: truncateString(task.logBuffer.join('\n'), LOG_LENGTH_MAX)
|
||||
props:
|
||||
dockerfile: builtImage.dockerfile
|
||||
projectType: builtImage.projectType
|
||||
|
||||
# Times here are timestamps, so test whether they're null
|
||||
# before creating a date out of them, as `new Date(null)`
|
||||
# creates a date representing UNIX time 0.
|
||||
if (startTime = builtImage.startTime)
|
||||
image.props.startTime = new Date(startTime)
|
||||
if (endTime = builtImage.endTime)
|
||||
image.props.endTime = new Date(endTime)
|
||||
docker.getImage(image.name).inspect().get('Size').then (size) ->
|
||||
image.props.size = size
|
||||
.return(image)
|
||||
.tap (images) ->
|
||||
summary = _(images).map ({ serviceName, props }) ->
|
||||
[ serviceName, "Image size: #{humanize.filesize(props.size)}" ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
renderer.end(summary)
|
||||
.finally(renderer.end)
|
||||
|
||||
createRelease = (apiEndpoint, auth, userId, appId, composition) ->
|
||||
_ = require('lodash')
|
||||
crypto = require('crypto')
|
||||
releaseMod = require('resin-release')
|
||||
|
||||
client = releaseMod.createClient({ apiEndpoint, auth })
|
||||
|
||||
releaseMod.create
|
||||
client: client
|
||||
user: userId
|
||||
application: appId
|
||||
composition: composition
|
||||
source: 'local'
|
||||
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase()
|
||||
.then ({ release, serviceImages }) ->
|
||||
release = _.omit(release, [
|
||||
'created_at'
|
||||
'belongs_to__application'
|
||||
'is_created_by__user'
|
||||
'__metadata'
|
||||
])
|
||||
serviceImages = _.mapValues serviceImages, (serviceImage) ->
|
||||
_.omit(serviceImage, [
|
||||
'created_at'
|
||||
'is_a_build_of__service'
|
||||
'__metadata'
|
||||
])
|
||||
return { client, release, serviceImages }
|
||||
|
||||
tagServiceImages = (docker, images, serviceImages) ->
|
||||
Promise.map images, (d) ->
|
||||
serviceImage = serviceImages[d.serviceName]
|
||||
imageName = serviceImage.is_stored_at__image_location
|
||||
# coffeelint: disable-next-line=check_scope ("Variable is assigned to but never read")
|
||||
[ _match, registry, repo, tag = 'latest' ] = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName)
|
||||
name = "#{registry}/#{repo}"
|
||||
docker.getImage(d.name).tag({ repo: name, tag, force: true })
|
||||
.then ->
|
||||
docker.getImage("#{name}:#{tag}")
|
||||
.then (localImage) ->
|
||||
serviceName: d.serviceName
|
||||
serviceImage: serviceImage
|
||||
localImage: localImage
|
||||
registry: registry
|
||||
repo: repo
|
||||
logs: d.logs
|
||||
props: d.props
|
||||
|
||||
|
||||
getPreviousRepos = (sdk, docker, logger, appID) ->
|
||||
sdk.pine.get(
|
||||
resource: 'release'
|
||||
options:
|
||||
$filter:
|
||||
belongs_to__application: appID
|
||||
status: 'success'
|
||||
$select:
|
||||
[ 'id' ]
|
||||
$expand:
|
||||
contains__image:
|
||||
$expand: 'image'
|
||||
$orderby: 'id desc'
|
||||
$top: 1
|
||||
)
|
||||
.then (release) ->
|
||||
# grab all images from the latest release, return all image locations in the registry
|
||||
if release?.length > 0
|
||||
images = release[0].contains__image
|
||||
Promise.map images, (d) ->
|
||||
imageName = d.image[0].is_stored_at__image_location
|
||||
docker.getRegistryAndName(imageName)
|
||||
.then ( registry ) ->
|
||||
logger.logDebug("Requesting access to previously pushed image repo (#{registry.imageName})")
|
||||
return registry.imageName
|
||||
.catch (e) ->
|
||||
logger.logDebug("Failed to access previously pushed image repo: #{e}")
|
||||
|
||||
authorizePush = (sdk, logger, tokenAuthEndpoint, registry, images, previousRepos) ->
|
||||
_ = require('lodash')
|
||||
|
||||
if not _.isArray(images)
|
||||
images = [ images ]
|
||||
|
||||
images.push previousRepos...
|
||||
sdk.request.send
|
||||
baseUrl: tokenAuthEndpoint
|
||||
url: '/auth/v1/token'
|
||||
qs:
|
||||
service: registry
|
||||
scope: images.map (repo) ->
|
||||
"repository:#{repo}:pull,push"
|
||||
.get('body')
|
||||
.get('token')
|
||||
.catchReturn({})
|
||||
|
||||
pushAndUpdateServiceImages = (docker, token, images, afterEach) ->
|
||||
chalk = require('chalk')
|
||||
{ DockerProgress } = require('docker-progress')
|
||||
{ retry } = require('./helpers')
|
||||
tty = require('./tty')(process.stdout)
|
||||
|
||||
opts = { authconfig: registrytoken: token }
|
||||
|
||||
progress = new DockerProgress(dockerToolbelt: docker)
|
||||
renderer = pushProgressRenderer(tty, chalk.blue('[Push]') + ' ')
|
||||
reporters = progress.aggregateProgress(images.length, renderer)
|
||||
|
||||
Promise.using tty.cursorHidden(), ->
|
||||
Promise.map images, ({ serviceImage, localImage, props, logs }, index) ->
|
||||
Promise.join(
|
||||
localImage.inspect().get('Size')
|
||||
retry(
|
||||
-> progress.push(localImage.name, reporters[index], opts)
|
||||
3 # `times` - retry 3 times
|
||||
localImage.name # `label` included in retry log messages
|
||||
2000 # `delayMs` - wait 2 seconds before the 1st retry
|
||||
1.4 # `backoffScaler` - wait multiplier for each retry
|
||||
).finally(renderer.end)
|
||||
(size, digest) ->
|
||||
serviceImage.image_size = size
|
||||
serviceImage.content_hash = digest
|
||||
serviceImage.build_log = logs
|
||||
serviceImage.dockerfile = props.dockerfile
|
||||
serviceImage.project_type = props.projectType
|
||||
serviceImage.start_timestamp = props.startTime if props.startTime
|
||||
serviceImage.end_timestamp = props.endTime if props.endTime
|
||||
serviceImage.push_timestamp = new Date()
|
||||
serviceImage.status = 'success'
|
||||
)
|
||||
.tapCatch (e) ->
|
||||
serviceImage.error_message = '' + e
|
||||
serviceImage.status = 'failed'
|
||||
.finally ->
|
||||
afterEach?(serviceImage, props)
|
||||
|
||||
exports.deployProject = (
|
||||
docker, logger,
|
||||
composition, images,
|
||||
appId, userId, auth,
|
||||
apiEndpoint,
|
||||
skipLogUpload
|
||||
) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
releaseMod = require('resin-release')
|
||||
tty = require('./tty')(process.stdout)
|
||||
|
||||
prefix = chalk.cyan('[Info]') + ' '
|
||||
spinner = createSpinner()
|
||||
runloop = runSpinner(tty, spinner, "#{prefix}Creating release...")
|
||||
|
||||
createRelease(apiEndpoint, auth, userId, appId, composition)
|
||||
.finally(runloop.end)
|
||||
.then ({ client, release, serviceImages }) ->
|
||||
logger.logDebug('Tagging images...')
|
||||
tagServiceImages(docker, images, serviceImages)
|
||||
.tap (images) ->
|
||||
logger.logDebug('Authorizing push...')
|
||||
sdk = require('balena-sdk').fromSharedOptions()
|
||||
getPreviousRepos(sdk, docker, logger, appId)
|
||||
.then (previousRepos) ->
|
||||
authorizePush(sdk, logger, apiEndpoint, images[0].registry, _.map(images, 'repo'), previousRepos)
|
||||
.then (token) ->
|
||||
logger.logInfo('Pushing images to registry...')
|
||||
pushAndUpdateServiceImages docker, token, images, (serviceImage) ->
|
||||
logger.logDebug("Saving image #{serviceImage.is_stored_at__image_location}")
|
||||
if skipLogUpload
|
||||
delete serviceImage.build_log
|
||||
releaseMod.updateImage(client, serviceImage.id, serviceImage)
|
||||
.finally ->
|
||||
logger.logDebug('Untagging images...')
|
||||
Promise.map images, ({ localImage }) ->
|
||||
localImage.remove()
|
||||
.then ->
|
||||
release.status = 'success'
|
||||
.tapCatch ->
|
||||
release.status = 'failed'
|
||||
.finally ->
|
||||
runloop = runSpinner(tty, spinner, "#{prefix}Saving release...")
|
||||
release.end_timestamp = new Date()
|
||||
releaseMod.updateRelease(client, release.id, release)
|
||||
.finally(runloop.end)
|
||||
.return(release)
|
||||
|
||||
# utilities
|
||||
|
||||
renderProgressBar = (percentage, stepCount) ->
|
||||
_ = require('lodash')
|
||||
percentage = _.clamp(percentage, 0, 100)
|
||||
barCount = stepCount * percentage // 100
|
||||
spaceCount = stepCount - barCount
|
||||
bar = "[#{_.repeat('=', barCount)}>#{_.repeat(' ', spaceCount)}]"
|
||||
return "#{bar} #{_.padStart(percentage, 3)}%"
|
||||
|
||||
pushProgressRenderer = (tty, prefix) ->
|
||||
fn = (e) ->
|
||||
{ error, percentage } = e
|
||||
throw new Error(error) if error?
|
||||
bar = renderProgressBar(percentage, 40)
|
||||
tty.replaceLine("#{prefix}#{bar}\r")
|
||||
fn.end = ->
|
||||
tty.clearLine()
|
||||
return fn
|
||||
|
||||
createLogStream = (input) ->
|
||||
split = require('split')
|
||||
stripAnsi = require('strip-ansi-stream')
|
||||
return input.pipe(stripAnsi()).pipe(split())
|
||||
|
||||
dropEmptyLinesStream = ->
|
||||
through = require('through2')
|
||||
through (data, enc, cb) ->
|
||||
str = data.toString('utf-8')
|
||||
@push(str) if str.trim()
|
||||
cb()
|
||||
|
||||
buildLogCapture = (objectMode, buffer) ->
|
||||
through = require('through2')
|
||||
|
||||
through { objectMode }, (data, enc, cb) ->
|
||||
# data from pull stream
|
||||
if data.error
|
||||
buffer.push("#{data.error}")
|
||||
else if data.progress and data.status
|
||||
buffer.push("#{data.progress}% #{data.status}")
|
||||
else if data.status
|
||||
buffer.push("#{data.status}")
|
||||
|
||||
# data from build stream
|
||||
else
|
||||
buffer.push(data)
|
||||
|
||||
cb(null, data)
|
||||
|
||||
buildProgressAdapter = (inline) ->
|
||||
through = require('through2')
|
||||
|
||||
stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/
|
||||
|
||||
[ step, numSteps, progress ] = [ null, null, undefined ]
|
||||
|
||||
through { objectMode: true }, (str, enc, cb) ->
|
||||
return cb(null, str) if not str?
|
||||
|
||||
if inline
|
||||
return cb(null, { status: str })
|
||||
|
||||
if /^Successfully tagged /.test(str)
|
||||
progress = undefined
|
||||
else
|
||||
if (match = stepRegex.exec(str))
|
||||
step = match[1]
|
||||
numSteps ?= match[2]
|
||||
str = match[3]
|
||||
if step?
|
||||
str = "Step #{step}/#{numSteps}: #{str}"
|
||||
progress = parseInt(step, 10) * 100 // parseInt(numSteps, 10)
|
||||
|
||||
cb(null, { status: str, progress })
|
||||
|
||||
pullProgressAdapter = (outStream) ->
|
||||
return ({ status, id, percentage, error, errorDetail }) ->
|
||||
if status?
|
||||
status = status.replace(/^Status: /, '')
|
||||
if id?
|
||||
status = "#{id}: #{status}"
|
||||
if percentage is 100
|
||||
percentage = undefined
|
||||
outStream.write
|
||||
status: status
|
||||
progress: percentage
|
||||
error: errorDetail?.message ? error
|
||||
|
||||
createSpinner = ->
|
||||
chars = '|/-\\'
|
||||
index = 0
|
||||
-> chars[(index++) % chars.length]
|
||||
|
||||
runSpinner = (tty, spinner, msg) ->
|
||||
runloop = createRunLoop ->
|
||||
tty.clearLine()
|
||||
tty.writeLine("#{msg} #{spinner()}")
|
||||
tty.cursorUp()
|
||||
runloop.onEnd = ->
|
||||
tty.clearLine()
|
||||
tty.writeLine(msg)
|
||||
return runloop
|
||||
|
||||
createRunLoop = (tick) ->
|
||||
timerId = setInterval(tick, 1000 / 10)
|
||||
runloop = {
|
||||
onEnd: ->
|
||||
end: ->
|
||||
clearInterval(timerId)
|
||||
runloop.onEnd()
|
||||
}
|
||||
return runloop
|
||||
|
||||
class BuildProgressUI
|
||||
constructor: (tty, descriptors) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
through = require('through2')
|
||||
|
||||
eventHandler = @_handleEvent
|
||||
services = _.map(descriptors, 'serviceName')
|
||||
|
||||
streams = _(services).map (service) ->
|
||||
stream = through.obj (event, _enc, cb) ->
|
||||
eventHandler(service, event)
|
||||
cb()
|
||||
stream.pipe(tty.stream)
|
||||
[ service, stream ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
@_tty = tty
|
||||
@_serviceToDataMap = {}
|
||||
@_services = services
|
||||
|
||||
# Logger magically prefixes the log line with [Build] etc., but it doesn't
|
||||
# work well with the spinner we're also showing. Manually build the prefix
|
||||
# here and bypass the logger.
|
||||
prefix = chalk.blue('[Build]') + ' '
|
||||
|
||||
offset = 10 # account for escape sequences inserted for colouring
|
||||
@_prefixWidth = offset + prefix.length + _.max(_.map(services, 'length'))
|
||||
@_prefix = prefix
|
||||
|
||||
# these are to handle window wrapping
|
||||
@_maxLineWidth = null
|
||||
@_lineWidths = []
|
||||
|
||||
@_startTime = null
|
||||
@_ended = false
|
||||
@_cancelled = false
|
||||
@_spinner = createSpinner()
|
||||
|
||||
@streams = streams
|
||||
|
||||
_handleEvent: (service, event) =>
|
||||
@_serviceToDataMap[service] = event
|
||||
|
||||
_handleInterrupt: =>
|
||||
@_cancelled = true
|
||||
@end()
|
||||
process.exit(130) # 128 + SIGINT
|
||||
|
||||
start: =>
|
||||
process.on('SIGINT', @_handleInterrupt)
|
||||
@_tty.hideCursor()
|
||||
@_services.forEach (service) =>
|
||||
@streams[service].write({ status: 'Preparing...' })
|
||||
@_runloop = createRunLoop(@_display)
|
||||
@_startTime = Date.now()
|
||||
|
||||
end: (summary = null) =>
|
||||
return if @_ended
|
||||
@_ended = true
|
||||
process.removeListener('SIGINT', @_handleInterrupt)
|
||||
@_runloop.end()
|
||||
@_runloop = null
|
||||
|
||||
@_clear()
|
||||
@_renderStatus(true)
|
||||
@_renderSummary(summary ? @_getServiceSummary())
|
||||
@_tty.showCursor()
|
||||
|
||||
_display: =>
|
||||
@_clear()
|
||||
@_renderStatus()
|
||||
@_renderSummary(@_getServiceSummary())
|
||||
@_tty.cursorUp(@_services.length + 1) # for status line
|
||||
|
||||
_clear: ->
|
||||
@_tty.deleteToEnd()
|
||||
@_maxLineWidth = @_tty.currentWindowSize().width
|
||||
|
||||
_getServiceSummary: ->
|
||||
_ = require('lodash')
|
||||
|
||||
services = @_services
|
||||
serviceToDataMap = @_serviceToDataMap
|
||||
|
||||
_(services).map (service) ->
|
||||
{ status, progress, error } = serviceToDataMap[service] ? {}
|
||||
if error
|
||||
return "#{error}"
|
||||
else if progress
|
||||
bar = renderProgressBar(progress, 20)
|
||||
return "#{bar} #{status}" if status
|
||||
return "#{bar}"
|
||||
else if status
|
||||
return "#{status}"
|
||||
else
|
||||
return 'Waiting...'
|
||||
.map (data, index) ->
|
||||
[ services[index], data ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
_renderStatus: (end = false) ->
|
||||
moment = require('moment')
|
||||
require('moment-duration-format')(moment)
|
||||
|
||||
@_tty.clearLine()
|
||||
@_tty.write(@_prefix)
|
||||
if end and @_cancelled
|
||||
@_tty.writeLine('Build cancelled')
|
||||
else if end
|
||||
serviceCount = @_services.length
|
||||
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
|
||||
runTime = Date.now() - @_startTime
|
||||
durationStr = moment.duration(runTime // 1000, 'seconds').format()
|
||||
@_tty.writeLine("Built #{serviceStr} in #{durationStr}")
|
||||
else
|
||||
@_tty.writeLine("Building services... #{@_spinner()}")
|
||||
|
||||
_renderSummary: (serviceToStrMap) ->
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
truncate = require('cli-truncate')
|
||||
strlen = require('string-width')
|
||||
|
||||
@_services.forEach (service, index) =>
|
||||
str = _.padEnd(@_prefix + chalk.bold(service), @_prefixWidth)
|
||||
str += serviceToStrMap[service]
|
||||
if @_maxLineWidth?
|
||||
str = truncate(str, @_maxLineWidth)
|
||||
@_lineWidths[index] = strlen(str)
|
||||
|
||||
@_tty.clearLine()
|
||||
@_tty.writeLine(str)
|
||||
|
||||
class BuildProgressInline
|
||||
constructor: (outStream, descriptors) ->
|
||||
_ = require('lodash')
|
||||
through = require('through2')
|
||||
|
||||
services = _.map(descriptors, 'serviceName')
|
||||
eventHandler = @_renderEvent
|
||||
streams = _(services).map (service) ->
|
||||
stream = through.obj (event, _enc, cb) ->
|
||||
eventHandler(service, event)
|
||||
cb()
|
||||
stream.pipe(outStream)
|
||||
[ service, stream ]
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
offset = 10 # account for escape sequences inserted for colouring
|
||||
@_prefixWidth = offset + _.max(_.map(services, 'length'))
|
||||
@_outStream = outStream
|
||||
@_services = services
|
||||
@_startTime = null
|
||||
@_ended = false
|
||||
|
||||
@streams = streams
|
||||
|
||||
start: =>
|
||||
@_outStream.write('Building services...\n')
|
||||
@_services.forEach (service) =>
|
||||
@streams[service].write({ status: 'Preparing...' })
|
||||
@_startTime = Date.now()
|
||||
|
||||
end: (summary = null) =>
|
||||
moment = require('moment')
|
||||
require('moment-duration-format')(moment)
|
||||
|
||||
return if @_ended
|
||||
@_ended = true
|
||||
|
||||
if summary?
|
||||
@_services.forEach (service) =>
|
||||
@_renderEvent(service, summary[service])
|
||||
|
||||
if @_cancelled
|
||||
@_outStream.write('Build cancelled\n')
|
||||
else
|
||||
serviceCount = @_services.length
|
||||
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
|
||||
runTime = Date.now() - @_startTime
|
||||
durationStr = moment.duration(runTime // 1000, 'seconds').format()
|
||||
@_outStream.write("Built #{serviceStr} in #{durationStr}\n")
|
||||
|
||||
_renderEvent: (service, event) =>
|
||||
_ = require('lodash')
|
||||
chalk = require('chalk')
|
||||
|
||||
str = do ->
|
||||
{ status, error } = event
|
||||
if error
|
||||
return "#{error}"
|
||||
else if status
|
||||
return "#{status}"
|
||||
else
|
||||
return 'Waiting...'
|
||||
|
||||
prefix = _.padEnd(chalk.bold(service), @_prefixWidth)
|
||||
@_outStream.write(prefix)
|
||||
@_outStream.write(str)
|
||||
@_outStream.write('\n')
|
55
lib/utils/compose.d.ts
vendored
Normal file
55
lib/utils/compose.d.ts
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 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 * as Bluebird from 'bluebird';
|
||||
import { Composition } from 'resin-compose-parse';
|
||||
import * as Stream from 'stream';
|
||||
import { Pack } from 'tar-stream';
|
||||
|
||||
import Logger = require('./logger');
|
||||
|
||||
interface Image {
|
||||
context: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface Descriptor {
|
||||
image: Image | string;
|
||||
serviceName: string;
|
||||
}
|
||||
|
||||
export function resolveProject(projectRoot: string): Bluebird<string>;
|
||||
|
||||
export interface ComposeProject {
|
||||
path: string;
|
||||
name: string;
|
||||
composition: Composition;
|
||||
descriptors: Descriptor[];
|
||||
}
|
||||
|
||||
export function loadProject(
|
||||
logger: Logger,
|
||||
projectPath: string,
|
||||
projectName: string,
|
||||
image?: string,
|
||||
dockerfilePath?: string,
|
||||
): Bluebird<ComposeProject>;
|
||||
|
||||
export function tarDirectory(
|
||||
source: string,
|
||||
preFinalizeCallback?: (pack: Pack) => void,
|
||||
): Promise<Stream.Readable>;
|
254
lib/utils/compose_ts.ts
Normal file
254
lib/utils/compose_ts.ts
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 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 * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import { Composition } from 'resin-compose-parse';
|
||||
import * as MultiBuild from 'resin-multibuild';
|
||||
import { Readable } from 'stream';
|
||||
import * as tar from 'tar-stream';
|
||||
|
||||
import { BalenaSDK } from 'balena-sdk';
|
||||
import { DeviceInfo } from './device/api';
|
||||
import Logger = require('./logger');
|
||||
|
||||
export interface RegistrySecrets {
|
||||
[registryAddress: string]: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getRegistrySecrets(
|
||||
sdk: BalenaSDK,
|
||||
inputFilename?: string,
|
||||
): Promise<RegistrySecrets> {
|
||||
const { fs } = await import('mz');
|
||||
const Path = await import('path');
|
||||
|
||||
if (inputFilename != null) {
|
||||
return await parseRegistrySecrets(inputFilename);
|
||||
}
|
||||
|
||||
const directory = await sdk.settings.get('dataDirectory');
|
||||
const potentialPaths = [
|
||||
Path.join(directory, 'secrets.yml'),
|
||||
Path.join(directory, 'secrets.yaml'),
|
||||
Path.join(directory, 'secrets.json'),
|
||||
];
|
||||
|
||||
for (const path of potentialPaths) {
|
||||
if (await fs.exists(path)) {
|
||||
return await parseRegistrySecrets(path);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async function parseRegistrySecrets(
|
||||
secretsFilename: string,
|
||||
): Promise<RegistrySecrets> {
|
||||
const { fs } = await import('mz');
|
||||
const { exitWithExpectedError } = await import('../utils/patterns');
|
||||
try {
|
||||
let isYaml = false;
|
||||
if (/.+\.ya?ml$/i.test(secretsFilename)) {
|
||||
isYaml = true;
|
||||
} else if (!/.+\.json$/i.test(secretsFilename)) {
|
||||
throw new Error('Filename must end with .json, .yml or .yaml');
|
||||
}
|
||||
const raw = (await fs.readFile(secretsFilename)).toString();
|
||||
const registrySecrets = new MultiBuild.RegistrySecretValidator().validateRegistrySecrets(
|
||||
isYaml ? require('js-yaml').safeLoad(raw) : JSON.parse(raw),
|
||||
);
|
||||
MultiBuild.addCanonicalDockerHubEntry(registrySecrets);
|
||||
return registrySecrets;
|
||||
} catch (error) {
|
||||
return exitWithExpectedError(
|
||||
`Error validating registry secrets file "${secretsFilename}":\n${
|
||||
error.message
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the compose-specific command-line options defined in compose.coffee.
|
||||
* This function is meant to be called very early on to validate users' input,
|
||||
* before any project loading / building / deploying.
|
||||
*/
|
||||
export async function validateComposeOptions(
|
||||
sdk: BalenaSDK,
|
||||
options: { [opt: string]: any },
|
||||
) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a BuildTask array of "resolved build tasks" by calling multibuild
|
||||
* .splitBuildStream() and performResolution(), and add build stream error
|
||||
* handlers and debug logging.
|
||||
* Both `balena build` and `balena deploy` call this function.
|
||||
*/
|
||||
export async function makeBuildTasks(
|
||||
composition: Composition,
|
||||
tarStream: Readable,
|
||||
deviceInfo: DeviceInfo,
|
||||
logger: Logger,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const buildTasks = await MultiBuild.splitBuildStream(composition, tarStream);
|
||||
|
||||
logger.logDebug('Found build tasks:');
|
||||
_.each(buildTasks, task => {
|
||||
let infoStr: string;
|
||||
if (task.external) {
|
||||
infoStr = `image pull [${task.imageName}]`;
|
||||
} else {
|
||||
infoStr = `build [${task.context}]`;
|
||||
}
|
||||
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
|
||||
});
|
||||
|
||||
logger.logDebug(
|
||||
`Resolving services with [${deviceInfo.deviceType}|${deviceInfo.arch}]`,
|
||||
);
|
||||
|
||||
await performResolution(buildTasks, deviceInfo);
|
||||
|
||||
logger.logDebug('Found project types:');
|
||||
_.each(buildTasks, task => {
|
||||
if (task.external) {
|
||||
logger.logDebug(` ${task.serviceName}: External image`);
|
||||
} else {
|
||||
logger.logDebug(` ${task.serviceName}: ${task.projectType}`);
|
||||
}
|
||||
});
|
||||
|
||||
return buildTasks;
|
||||
}
|
||||
|
||||
async function performResolution(
|
||||
tasks: MultiBuild.BuildTask[],
|
||||
deviceInfo: DeviceInfo,
|
||||
): Promise<MultiBuild.BuildTask[]> {
|
||||
const { cloneTarStream } = require('tar-utils');
|
||||
|
||||
return await new Promise<MultiBuild.BuildTask[]>((resolve, reject) => {
|
||||
const buildTasks = MultiBuild.performResolution(
|
||||
tasks,
|
||||
deviceInfo.arch,
|
||||
deviceInfo.deviceType,
|
||||
{ error: [reject] },
|
||||
);
|
||||
// Do one task at a time (Bluebird.each instead of Bluebird.all)
|
||||
// in order to reduce peak memory usage. Resolves to buildTasks.
|
||||
Bluebird.each(buildTasks, buildTask => {
|
||||
// buildStream is falsy for "external" tasks (image pull)
|
||||
if (!buildTask.buildStream) {
|
||||
return buildTask;
|
||||
}
|
||||
// Consume each task.buildStream in order to trigger the
|
||||
// resolution events that define fields like:
|
||||
// task.dockerfile, task.dockerfilePath,
|
||||
// task.projectType, task.resolved
|
||||
// This mimics what is currently done in `resin-builder`.
|
||||
return cloneTarStream(buildTask.buildStream).then(
|
||||
(clonedStream: tar.Pack) => {
|
||||
buildTask.buildStream = clonedStream;
|
||||
if (!buildTask.external && !buildTask.resolved) {
|
||||
throw new Error(
|
||||
`Project type for service "${
|
||||
buildTask.serviceName
|
||||
}" could not be determined. Missing a Dockerfile?`,
|
||||
);
|
||||
}
|
||||
return buildTask;
|
||||
},
|
||||
);
|
||||
}).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce that, for example, if 'myProject/MyDockerfile.template' is specified
|
||||
* as an alternativate Dockerfile name, then 'myProject/MyDockerfile' must not
|
||||
* exist.
|
||||
* @param projectPath The project source folder (-s command-line option)
|
||||
* @param dockerfilePath The alternative Dockerfile specified by the user
|
||||
*/
|
||||
export function validateSpecifiedDockerfile(
|
||||
projectPath: string,
|
||||
dockerfilePath: string = '',
|
||||
): string {
|
||||
if (!dockerfilePath) {
|
||||
return dockerfilePath;
|
||||
}
|
||||
const { exitWithExpectedError } = require('../utils/patterns');
|
||||
const { isAbsolute, join, normalize, parse, posix } = require('path');
|
||||
const { existsSync } = require('fs');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const { contains, toNativePath, toPosixPath } = MultiBuild.PathUtils;
|
||||
|
||||
// reminder: native windows paths may start with a drive specificaton,
|
||||
// e.g. 'C:\absolute' or 'C:relative'.
|
||||
if (isAbsolute(dockerfilePath) || posix.isAbsolute(dockerfilePath)) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Error: absolute Dockerfile path detected:
|
||||
"${dockerfilePath}"
|
||||
The Dockerfile path should be relative to the source folder.
|
||||
`);
|
||||
}
|
||||
const nativeProjectPath = normalize(projectPath);
|
||||
const nativeDockerfilePath = join(projectPath, toNativePath(dockerfilePath));
|
||||
|
||||
if (!contains(nativeProjectPath, nativeDockerfilePath)) {
|
||||
// Note that testing the existence of nativeDockerfilePath in the
|
||||
// filesystem (after joining its path to the source folder) is not
|
||||
// sufficient, because the user could have added '../' to the path.
|
||||
exitWithExpectedError(stripIndent`
|
||||
Error: the specified Dockerfile must be in a subfolder of the source folder:
|
||||
Specified dockerfile: "${nativeDockerfilePath}"
|
||||
Source folder: "${nativeProjectPath}"
|
||||
`);
|
||||
}
|
||||
|
||||
if (!existsSync(nativeDockerfilePath)) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Error: Dockerfile not found: "${nativeDockerfilePath}"
|
||||
`);
|
||||
}
|
||||
|
||||
const { dir, ext, name } = parse(nativeDockerfilePath);
|
||||
if (ext) {
|
||||
const nativePathMinusExt = join(dir, name);
|
||||
|
||||
if (existsSync(nativePathMinusExt)) {
|
||||
exitWithExpectedError(stripIndent`
|
||||
Error: "${name}" exists on the same folder as "${dockerfilePath}".
|
||||
When an alternative Dockerfile name is specified, a file with the same
|
||||
base name (minus the file extension) must not exist in the same folder.
|
||||
This is because the base name file will be auto generated and added to
|
||||
the tar stream that is sent to the docker daemon, resulting in duplicate
|
||||
Dockerfiles and undefined behavior.
|
||||
`);
|
||||
}
|
||||
}
|
||||
return posix.normalize(toPosixPath(dockerfilePath));
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
exports.generateBaseConfig = (application, options) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
deviceConfig = require('resin-device-config')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
options = _.mapValues options, (value, key) ->
|
||||
if key == 'appUpdatePollInterval'
|
||||
value * 60 * 1000
|
||||
else
|
||||
value
|
||||
|
||||
Promise.props
|
||||
userId: resin.auth.getUserId()
|
||||
username: resin.auth.whoami()
|
||||
apiUrl: resin.settings.get('apiUrl')
|
||||
vpnUrl: resin.settings.get('vpnUrl')
|
||||
registryUrl: resin.settings.get('registryUrl')
|
||||
deltaUrl: resin.settings.get('deltaUrl')
|
||||
pubNubKeys: resin.models.config.getPubNubKeys()
|
||||
mixpanelToken: resin.models.config.getMixpanelToken()
|
||||
.then (results) ->
|
||||
deviceConfig.generate
|
||||
application: application
|
||||
user:
|
||||
id: results.userId
|
||||
username: results.username
|
||||
endpoints:
|
||||
api: results.apiUrl
|
||||
vpn: results.vpnUrl
|
||||
registry: results.registryUrl
|
||||
delta: results.deltaUrl
|
||||
pubnub: results.pubNubKeys
|
||||
mixpanel:
|
||||
token: results.mixpanelToken
|
||||
, options
|
||||
|
||||
exports.generateApplicationConfig = (application, options) ->
|
||||
exports.generateBaseConfig(application, options)
|
||||
.tap (config) ->
|
||||
authenticateWithApplicationKey(config, application.id)
|
||||
|
||||
exports.generateDeviceConfig = (device, deviceApiKey, options) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
resin.models.application.get(device.application_name)
|
||||
.then (application) ->
|
||||
exports.generateBaseConfig(application, options)
|
||||
.tap (config) ->
|
||||
# Device API keys are only safe for ResinOS 2.0.3+. We could somehow obtain
|
||||
# the expected version for this config and generate one when we know it's safe,
|
||||
# but instead for now we fall back to app keys unless the user has explicitly opted in.
|
||||
if deviceApiKey?
|
||||
authenticateWithDeviceKey(config, device.uuid, deviceApiKey)
|
||||
else
|
||||
authenticateWithApplicationKey(config, application.id)
|
||||
.then (config) ->
|
||||
# Associate a device, to prevent the supervisor
|
||||
# from creating another one on its own.
|
||||
config.registered_at = Math.floor(Date.now() / 1000)
|
||||
config.deviceId = device.id
|
||||
config.uuid = device.uuid
|
||||
|
||||
return config
|
||||
|
||||
authenticateWithApplicationKey = (config, applicationNameOrId) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.application.generateApiKey(applicationNameOrId)
|
||||
.then (apiKey) ->
|
||||
config.apiKey = apiKey
|
||||
return config
|
||||
|
||||
authenticateWithDeviceKey = (config, uuid, customDeviceApiKey) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
customDeviceApiKey || resin.models.device.generateDeviceKey(uuid)
|
||||
.then (deviceApiKey) ->
|
||||
config.deviceApiKey = deviceApiKey
|
||||
return config
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user