mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
1614 Commits
Author | SHA1 | Date | |
---|---|---|---|
f47a4ccb2b | |||
ece3c06786 | |||
b50fef8cb4 | |||
4f7ea7e32a | |||
01c12dd5bf | |||
b114697cab | |||
14ce4d73b6 | |||
051e1f1331 | |||
c054d55456 | |||
133daefe83 | |||
232e9c3fc1 | |||
03eed32f12 | |||
e1d51305b0 | |||
af071beef0 | |||
6dce5c1212 | |||
a26c3912d3 | |||
f3d86ab37c | |||
420a282bea | |||
cfbf00f543 | |||
83888210d6 | |||
2f7dd54e37 | |||
ab9dabaf77 | |||
9e98e7142c | |||
4656f8f11d | |||
5b5f258685 | |||
7d2e32777f | |||
10120bb97f | |||
98c9469151 | |||
98c0991fe3 | |||
f686d3a9bf | |||
8dda8d89d5 | |||
7794158062 | |||
24d6b7fa38 | |||
f5f0c7b4a7 | |||
11d1a3f5a0 | |||
2859d16b31 | |||
70bfe4ce8e | |||
6f4db3176a | |||
ebb0ec5287 | |||
4995e9b642 | |||
795057338f | |||
ab673f884a | |||
907508bce1 | |||
599b897721 | |||
146de39b83 | |||
a3cab32b4e | |||
d709e06f48 | |||
98f101643d | |||
c619bd4b99 | |||
19c3069b22 | |||
7e1d58546c | |||
2c01f8adee | |||
3ecf461d55 | |||
06ab84fd10 | |||
a7b78d2ccd | |||
432109060e | |||
b32ae4a667 | |||
36e4b3249c | |||
41e5fdbe27 | |||
6dc0fe10bc | |||
656591fde4 | |||
d967b942e0 | |||
7e34fdfeeb | |||
995f8a3338 | |||
ff282205d5 | |||
be144fafa2 | |||
683037cd2f | |||
555096db6b | |||
a85c482416 | |||
523d563b4e | |||
1569915fae | |||
b1552f8e9b | |||
f455602c73 | |||
3e97669b3c | |||
728c4f4296 | |||
bf073942f0 | |||
b9290f4859 | |||
626d328194 | |||
21dd959344 | |||
5c8d822aee | |||
2ab8ae1c10 | |||
fcc13f9476 | |||
a38b41f339 | |||
2fc0728a09 | |||
040c4987fc | |||
254d9c49a4 | |||
6e5e1c4f5f | |||
d7213e868f | |||
d82b019480 | |||
1693bd91c0 | |||
e4f605d6ac | |||
fd7e7f57eb | |||
1d073af31a | |||
fcaaec1fff | |||
ac3a688d46 | |||
979284b071 | |||
bc4aa6006e | |||
2cad44915b | |||
889c7b08cf | |||
56a196210d | |||
a23759a1ba | |||
ba0024645d | |||
3cb184c8af | |||
644d54a113 | |||
a6f905b71c | |||
3b426e4a53 | |||
d241523d93 | |||
1c354c800b | |||
e5861a708e | |||
6a019af25f | |||
8522363cd3 | |||
480228d8f4 | |||
175413af34 | |||
d1b4560b37 | |||
77f3fa4b6c | |||
a21d3fe2d2 | |||
df440f0580 | |||
92bfa574e3 | |||
d33b7ec585 | |||
08d5a77734 | |||
744122b1b8 | |||
c3a8bb3de6 | |||
e50d92727e | |||
3bb5e495a6 | |||
803a9070fd | |||
a84ab793a0 | |||
81269e92d5 | |||
11d5deef4c | |||
c98bc3280d | |||
8c2a40cb39 | |||
2fdd023a64 | |||
aff370e9c3 | |||
be21c8d43e | |||
5b33826309 | |||
052c8d138e | |||
d0228f20fd | |||
4577d72ead | |||
0dde84ec0b | |||
81b620f55e | |||
4b056b4d4c | |||
5723e69267 | |||
2d341cac48 | |||
4e50d08f7b | |||
aff5cd9b0d | |||
2bb0933a42 | |||
8d60cd1f92 | |||
9756efb539 | |||
61ed6ff69d | |||
127560fa65 | |||
2611ea22f9 | |||
d7021a556e | |||
9412a21d40 | |||
f63be0b4cd | |||
c2561938c1 | |||
98a2c0635d | |||
ee54d638ad | |||
d9b044c1b8 | |||
dd20a8b00f | |||
01147c31a4 | |||
2f6889cca1 | |||
83286e6729 | |||
97def08ec5 | |||
1a57385626 | |||
1301f62981 | |||
6ae337db8a | |||
b84cdd6230 | |||
2f24e591ef | |||
597b894917 | |||
3b53b75626 | |||
9b1c3c665b | |||
153cdf4bb0 | |||
1d9a397f71 | |||
6167c7b8b3 | |||
cbcd7694a9 | |||
52bece7f17 | |||
4f6550e7eb | |||
ec17ed6ef2 | |||
0df6368ab9 | |||
b5cac122cf | |||
ae75e1396e | |||
3b519f0258 | |||
275fa9c16b | |||
a00db0f5d8 | |||
2a8eb3a6ed | |||
bcd49e0292 | |||
d8e1cd6597 | |||
52c2b041da | |||
d5d0486c3f | |||
6f51807e8c | |||
ab526c9ed8 | |||
14c5b27cdd | |||
ce01ce73b1 | |||
0f6d160b2e | |||
6d7d1956ea | |||
692eddf43f | |||
eb5cfecfaf | |||
73d6d7b264 | |||
14ced9f384 | |||
4d8cd1cc46 | |||
dbe9a727d5 | |||
8ac65c3800 | |||
4ae91ef846 | |||
7311cfa755 | |||
a200bf268d | |||
d398e22c58 | |||
b51d2fffbb | |||
5fef98bdf8 | |||
203ccaf97b | |||
a348528ed3 | |||
04c4250fba | |||
a97398950e | |||
f55376df32 | |||
3f285cc26d | |||
6d95c5bad5 | |||
6b33f95661 | |||
9ab34c2deb | |||
db247307db | |||
ad0b667bc7 | |||
d98bc9fb06 | |||
74cdd80b51 | |||
5c39952002 | |||
2874a69d7d | |||
6ec05e8dcf | |||
6602845202 | |||
e8cd4153c7 | |||
0cfa1a0dfb | |||
00ce3ab751 | |||
5c1323d583 | |||
d9f42b888d | |||
0db8c85fc8 | |||
bc601d07e3 | |||
e1a91035ae | |||
8dced8afe2 | |||
ffded6736a | |||
1a851f552e | |||
68b64016ab | |||
edac54ccfe | |||
560b0abbe7 | |||
b48d238be6 | |||
a10d5b9abe | |||
23f2242e22 | |||
f8612fd748 | |||
36446ff488 | |||
a5ce0436c7 | |||
3302e2f639 | |||
c3c1c5fc41 | |||
9f59b6dde5 | |||
8be56ef092 | |||
e9f8cadb73 | |||
3e4f9f9572 | |||
f7d4a37060 | |||
d0e268815a | |||
c3454d3abb | |||
6b0f645094 | |||
ada7801a0d | |||
da5e26f37e | |||
9447195c26 | |||
cd59496f11 | |||
9fda165d34 | |||
fe0ad92b43 | |||
81c5a62380 | |||
ebdd04ec73 | |||
e6264ced7a | |||
247f31a3cc | |||
a2b761ec4b | |||
e3672bc655 | |||
028141c0b0 | |||
e3c42cf63e | |||
0ae138db03 | |||
9350af9ddf | |||
88e4009e88 | |||
cb7692690d | |||
82e17cea6a | |||
9ed363da9e | |||
8aa4bd6173 | |||
5f098e7410 | |||
2de33d185a | |||
66b9f5a337 | |||
bbcb3a702f | |||
57d0014e32 | |||
8d9133e6a6 | |||
be82bcfa63 | |||
7c9a23451b | |||
1319e0642b | |||
e3b6db25d8 | |||
655534469a | |||
a8b0573699 | |||
99963cbb89 | |||
92715c3182 | |||
264c8535b4 | |||
159ee44d7e | |||
7e4b62c28a | |||
52b2ba6a30 | |||
cc1ba3d84e | |||
01d05fb148 | |||
cff9e50a22 | |||
eba2e7e4fb | |||
9a9d56b419 | |||
320b4864d9 | |||
7f79451376 | |||
68fa831843 | |||
3aa72dde4c | |||
f72d78954d | |||
cf87ca95a0 | |||
a50ca78eef | |||
4fe5a10029 | |||
9812239862 | |||
bc3fe29624 | |||
7e2ee7ab93 | |||
f151a208e5 | |||
292ad89b7e | |||
c062e6e876 | |||
dcb1c11700 | |||
96e28f3d45 | |||
ff319d67f3 | |||
f14e44a2e8 | |||
9aa6b0bc57 | |||
cbe12d5be7 | |||
c177f222ba | |||
d2fd1ec80a | |||
77873cf919 | |||
07c09c5f89 | |||
159cb752d1 | |||
a74f0413df | |||
43b1c5c24f | |||
45e7e9cb32 | |||
1a71bad8bb | |||
2d55df4704 | |||
6c0b3a5e53 | |||
3e955f3a91 | |||
30738d93b0 | |||
be76b8adbd | |||
d6a065a230 | |||
7b8e86372b | |||
bc15ad6e05 | |||
fcad35402a | |||
49b00e18ae | |||
a6ccd87069 | |||
0c1904fbdb | |||
e5d2661c96 | |||
eca3e91512 | |||
eb5ad08649 | |||
b3b22d6399 | |||
217cba819a | |||
c8275b52c3 | |||
47e85da789 | |||
c8cade95da | |||
a4de7143b1 | |||
6574745a23 | |||
1ee74df67e | |||
3e1b10007a | |||
448211e49c | |||
8658104647 | |||
6ec8bcddaa | |||
d138c40ebd | |||
f24c4a036c | |||
dabe81c31b | |||
c2f0f9a894 | |||
46b695cf22 | |||
9b79f79bac | |||
dddfad9dec | |||
0690554a94 | |||
62ea7518bc | |||
f30e486562 | |||
809a5fae25 | |||
eccb1bd9ad | |||
f859d5025a | |||
18d3ca3413 | |||
a826f16469 | |||
505c3ec7d3 | |||
47fa2a6151 | |||
b4b19637f4 | |||
5f552cf9a8 | |||
e42650f433 | |||
731bd909d6 | |||
2860535c45 | |||
122b5a0655 | |||
ec66c82d3f | |||
09a59ab03f | |||
3d2e109e7f | |||
26803067f1 | |||
7dc3977e82 | |||
10cbf514a2 | |||
e2114f73d7 | |||
2f448951c9 | |||
385d3e107b | |||
d98b2fa72f | |||
c6baa7a908 | |||
daa34feeda | |||
f813dad4d9 | |||
d7633b5f08 | |||
f44c2b777f | |||
bcfba693a5 | |||
08f40c0566 | |||
5a80654305 | |||
d2df2c7b60 | |||
36d3d1256e | |||
b77cb56cd0 | |||
524397fc9b | |||
ec73ee270b | |||
b83431c2e0 | |||
40c559322a | |||
2c5cf9dab6 | |||
ca8272b477 | |||
d6e7359400 | |||
af8d7283a5 | |||
9470e804c0 | |||
00943463a4 | |||
3f6d770233 | |||
c4a6086e9c | |||
4e61c00255 | |||
1713988e94 | |||
fe4e1d09d7 | |||
766695ceef | |||
e50a3270ba | |||
235c13bea9 | |||
62e4930e5b | |||
fb321b8c5b | |||
98152c0b09 | |||
0ab0e417b8 | |||
3642943896 | |||
7c62e34455 | |||
86af954f3b | |||
0c7947e185 | |||
48b281d7c6 | |||
8598223b61 | |||
cdd67e25f0 | |||
eac6bb5e5c | |||
077d1db9b7 | |||
d86f213b68 | |||
cdfd1d124b | |||
28c00696b8 | |||
dec570a6e2 | |||
9067558d18 | |||
4abdd71ce7 | |||
36f2f491b3 | |||
5e750b33c3 | |||
03053e125f | |||
bdc7c0fa39 | |||
ad4981328f | |||
3f35d6fde6 | |||
f2be811e18 | |||
6439aa5552 | |||
977fadab69 | |||
95c93d24da | |||
278d7fd02c | |||
59b9429570 | |||
9e870b08a7 | |||
671dca8287 | |||
a15060e9fc | |||
0738dd1520 | |||
5dbace353d | |||
e773549297 | |||
a1c406a479 | |||
5e196b8f63 | |||
054e59c6af | |||
88a1e413a3 | |||
1a74dcf4cf | |||
d48672fa93 | |||
9a7fcfffe8 | |||
f9ece2ce7d | |||
9d04e616a8 | |||
b8c7f23443 | |||
2b04763ac0 | |||
bff845a0e4 | |||
5076ca7532 | |||
93ba5832d8 | |||
af86ac73e6 | |||
173a48eede | |||
a4b34c109d | |||
69714a646b | |||
a41ef3764e | |||
f1220c6377 | |||
cefb3acc1f | |||
277da3ea9c | |||
99f84c2f6a | |||
a9c0899c32 | |||
8d3fb8fef5 | |||
4de41ce3e0 | |||
4b8cec652a | |||
2dd8e71adc | |||
05d478b759 | |||
9a7a364776 | |||
2cb5e28258 | |||
02e8429155 | |||
467afb3de6 | |||
324a406e7f | |||
17bf061853 | |||
6d543b79ff | |||
85aaf77e44 | |||
83c5684491 | |||
6bc4fbb750 | |||
1da96a0eb0 | |||
be209f1626 | |||
654d1dcff8 | |||
0a03e79d9d | |||
3f84045127 | |||
544f8fb4bd | |||
76997c99dc | |||
f4525bc11e | |||
f732c5bf5d | |||
2bc3348aff | |||
895be0be5d | |||
0f17129c2e | |||
9005affe64 | |||
4502f2a203 | |||
da3c11533c | |||
6acff945ef | |||
b3948d538c | |||
f53a69feb1 | |||
405b92114d | |||
27e1f3f7d7 | |||
1417875110 | |||
f58a49d6c3 | |||
f9743b269a | |||
0f5f65e0d3 | |||
58e7880f1d | |||
041823189f | |||
38194e6175 | |||
c04e9665ad | |||
1e37c97ffb | |||
913f09924a | |||
ceb47e9969 | |||
305755549e | |||
77931b314a | |||
b38b5b0b61 | |||
5cf407b483 | |||
8f6902f4cb | |||
751f67e997 | |||
be1a260af6 | |||
9db6961a7e | |||
b978230f9e | |||
cc5fe60a15 | |||
bbea58a9c8 | |||
56e35f6e9f | |||
95b5ac1c7f | |||
df3e1f1886 | |||
5d34659991 | |||
aca794b267 | |||
cd6072ac73 | |||
bda696ad8c | |||
ef4ee54a00 | |||
a2ca8e8f73 | |||
620a0abf31 | |||
95561864a6 | |||
51adfeaa3b | |||
76447a2177 | |||
a6153869e5 | |||
3466be1992 | |||
95843dd816 | |||
edd755d41c | |||
290c06074a | |||
dd7d9d1570 | |||
c4829153fc | |||
615f24edd3 | |||
a94e6d550e | |||
75044030cf | |||
046743071d | |||
4e95cb0cca | |||
4666019c84 | |||
323c9191b6 | |||
024bf2996b | |||
5210d474a9 | |||
3cce8d822c | |||
65250e431e | |||
29cc75598f | |||
33552724a1 | |||
c88b317143 | |||
658b0a5233 | |||
7fd436cd91 | |||
7c1faa6de0 | |||
90e184ea1f | |||
38920a1c59 | |||
df58ac7673 | |||
630d53311a | |||
b1eda160e8 | |||
a63c766f04 | |||
53325b7c05 | |||
622c510d65 | |||
890bea549f | |||
bb19903826 | |||
33210b896b | |||
c2a0e457c0 | |||
f464597069 | |||
02dcff5b67 | |||
2f4539b4d1 | |||
6c3429eb0c | |||
a8bd5d332a | |||
0462574d8d | |||
1325fb8c9a | |||
cf42dca777 | |||
afd97bd304 | |||
45ce442cf2 | |||
e8c5c9e9ef | |||
caac6855da | |||
e379900526 | |||
0bc1e7057f | |||
d94a74dfee | |||
a25a52c21b | |||
268bc36843 | |||
8e7eaaae24 | |||
b69ba0b617 | |||
1bedf937f8 | |||
74a521a271 | |||
6376ac28d5 | |||
2222a90884 | |||
09f04be77d | |||
636ecaf4e0 | |||
07fa504c78 | |||
5f74023bb8 | |||
b31e253fa4 | |||
f75f00e4d0 | |||
13deb83517 | |||
62b7d2fd1a | |||
aa5062ea6f | |||
394f2e0999 | |||
4a967b126b | |||
5f780a0947 | |||
0671ee0bef | |||
55b2b5a467 | |||
f7a4160c3f | |||
294290908b | |||
8ed4f547e0 | |||
3393e797d0 | |||
7de059b95e | |||
c2b0091f86 | |||
d96ad93e1e | |||
e0373e6f19 | |||
f50f169ff0 | |||
eada1ab87e | |||
51d4ffc5d9 | |||
59a7b9d12b | |||
3bb5ca50b2 | |||
353ee4a576 | |||
eaffc2574f | |||
5142274e9d | |||
a05b592f00 | |||
e7c89cf77c | |||
09444f0cff | |||
f329acc2ec | |||
ee3a0cc630 | |||
b40498786a | |||
43f551dd2e | |||
1f4382bea8 | |||
96bc8a829e | |||
0cdf4d95e5 | |||
a6ac0ee17b | |||
1b943bdf7d | |||
f831bb4645 | |||
82c486b202 | |||
02b888f7c1 | |||
69c97fed09 | |||
05cb89725e | |||
43ae9b672c | |||
27270dd589 | |||
0c5ed7adfb | |||
4280a3cd4a | |||
b8944ba65c | |||
a8fcd85f1a | |||
cc45d872c7 | |||
e0e76a1aa8 | |||
d9b417e9e5 | |||
101a4bc209 | |||
449461afae | |||
1987206b94 | |||
7dd33adfd1 | |||
63fe3d41cc | |||
59e4eb5143 | |||
78e627a471 | |||
3adc06d11b | |||
85fc9daa8b | |||
d463a2f0e5 | |||
863eae42c5 | |||
4b373d5ed4 | |||
4077da1491 | |||
3718473f05 | |||
54bcbb5d91 | |||
408f739e2d | |||
f481d5edae | |||
cff81ae86d | |||
f9d6a0ee72 | |||
9ea999f15f | |||
eac229ab7c | |||
7a865b2e15 | |||
32c588db55 | |||
aa670ad6f1 | |||
c1ba73a2da | |||
606b6c88ab | |||
a6f329750c | |||
c07b28e694 | |||
b3bef9e556 | |||
2ff427fb90 | |||
3d89b0c7a1 | |||
90db52db47 | |||
87004621ce | |||
de8089b0bb | |||
ae691391b6 | |||
a64b36fdb9 | |||
db5c473952 | |||
2b0bff8f16 | |||
2e7f606667 | |||
73e9f801e2 | |||
9a40f20004 | |||
6631fb5a69 | |||
f76ca1804a | |||
d25ad328f3 | |||
dfbcdc2c1b | |||
d484c957bb | |||
70714b6feb | |||
c9db5fd856 | |||
d54a709e7c | |||
09f20ecc1c | |||
b8b0221ce6 | |||
596d1bdc21 | |||
fb1dce9dbb | |||
fedfb603f6 | |||
8478b95e45 | |||
7bb2741a5a | |||
693a438d42 | |||
2a8e68cec2 | |||
e3435c66df | |||
192b751e57 | |||
d9643eb59e | |||
19c3178062 | |||
516fa90a20 | |||
56aabad8ad | |||
4a80beac35 | |||
cdedc58ec1 | |||
ed084edc48 | |||
b435a0e7ac | |||
731db63e78 | |||
2ee2bc8b02 | |||
b4c99dc03a | |||
730c09989d | |||
9e0733a143 | |||
8dd1106a44 | |||
4d389bb6cc | |||
6d6b3cb1a3 | |||
e84482fbbe | |||
f1d9c29786 | |||
34f4c1f6cc | |||
2390ddc02d | |||
70561705e5 | |||
05d58d8248 | |||
13610ef814 | |||
9c49890399 | |||
b9884ec545 | |||
ebd8f348ca | |||
90c4cbf2ae | |||
987e16cad0 | |||
15dfdc2229 | |||
35110e0610 | |||
bf722f61c7 | |||
08e8151b1f | |||
9977451b07 | |||
9f610a521e | |||
67a4e88e44 | |||
f0bd2f197d | |||
5fe908bbbf | |||
f9fc8fd2e8 | |||
9ea5198bda | |||
35732515ac | |||
3ea905dc68 | |||
062fadfa49 | |||
58983670f1 | |||
18fadf5634 | |||
80917b7198 | |||
f21dc3e2f4 | |||
d92e076829 | |||
cd491a7935 | |||
90269525be | |||
225408c57d | |||
8cfacc9cbc | |||
9a270539c6 | |||
df22d42412 | |||
49a7eb30c0 | |||
e41ea6fb1a | |||
636ec2a8f2 | |||
9eae9dcee3 | |||
6c26e1235c | |||
bacca5383a | |||
32e72c832f | |||
05aaed07b2 | |||
7c750f9e43 | |||
55bf4dc0f0 | |||
0afbd6f17a | |||
66b997d98c | |||
d485fd00a0 | |||
3322faeeb2 | |||
c32d894e97 | |||
ad737b8e02 | |||
bcc86fbcb6 | |||
d5c7527f8d | |||
5df65f67c3 | |||
79e65025cb | |||
dff6dafe85 | |||
adcc862acb | |||
8bf884d425 | |||
a6b282598b | |||
77089e31e4 | |||
7c6bae491f | |||
d5586e12d4 | |||
7e1f4791ed | |||
9d5ecb5f9c | |||
236dce37da | |||
a2ee48f2fb | |||
b74a0d1141 | |||
34d7b84d1e | |||
d999b901bb | |||
4a9d133c11 | |||
3a7604368a | |||
df2e611c42 | |||
30e48b658f | |||
f095ac169a | |||
f0030a1891 | |||
1d3af3245a | |||
f4612116b9 | |||
65ab3008e6 | |||
36026d8556 | |||
436ad60f4e | |||
e0ee333717 | |||
3b09c5ac91 | |||
6994499f14 | |||
9e19b5875b | |||
c3e5147a19 | |||
5e46815ac7 | |||
7b37c60e11 | |||
cf9fdbe6e4 | |||
66dfddc96d | |||
1fa2347608 | |||
6bed43fe1f | |||
46806c8377 | |||
cf93438df1 | |||
ea43130135 | |||
5e4daf8c3d | |||
20474aeb55 | |||
825213c02a | |||
13cef01374 | |||
7271f90dc6 | |||
8b5ebe0645 | |||
24e49bf131 | |||
5a0ef354f1 | |||
f8a9c10a77 | |||
bace8b5c1e | |||
d8c942c77e | |||
7fccd4a35e | |||
b78dd26f23 | |||
47c63e0e1d | |||
5d137f3c20 | |||
2bbdfda92e | |||
2f2f16267f | |||
051268168a | |||
2b264df41b | |||
eba193278e | |||
462b41b4ea | |||
ab5815c277 | |||
f75ffb53f5 | |||
3387f8f656 | |||
e8325e8268 | |||
c2491497b5 | |||
4596005a1f | |||
8d9cbbb526 | |||
17e51799f5 | |||
df797cdc2c | |||
57fc26c0f7 | |||
abc2cfd14c | |||
612fefcc65 | |||
0bbe376e41 | |||
04223dbc58 | |||
b5c4348de1 | |||
751749325f | |||
1e2e48b149 | |||
01b454351b | |||
6696b1b5f7 | |||
5da307f02e | |||
b391c96e64 | |||
0ee73f5164 | |||
1a1861bfcb | |||
717c43f10b | |||
dafbdd5f34 | |||
c204dbd6cd | |||
6e7f51758e | |||
ea89a6f221 | |||
94c9e13106 | |||
64c2f00d2a | |||
8f8d6b5f08 | |||
c49a1d3fbf | |||
abf573fa47 | |||
13e3e5e8ea | |||
faa558b432 | |||
4eea5e822b | |||
fe3e348128 | |||
7535b7110d | |||
0aaf6dff41 | |||
aca58743ea | |||
f6a262bcde | |||
9cc81866eb | |||
0607c2f231 | |||
fe0ba62026 | |||
f2af7b2588 | |||
e145540132 | |||
d21b84956c | |||
841ce9fd68 | |||
a4efc7c9c4 | |||
e6ecb0ec0b | |||
c420d0f63c | |||
f3ef7f6e18 | |||
e36435bb4c | |||
825964fdc6 | |||
5202e137d5 | |||
d23d837b8c | |||
8c537c112d | |||
106b971410 | |||
39cf86ed85 | |||
5de7a50fc0 | |||
ba4301487f | |||
f77156772a | |||
a6d6035725 | |||
5e0d24a1f1 | |||
9434570c2d | |||
674c0ca7b8 | |||
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 |
15
.gitattributes
vendored
Normal file
15
.gitattributes
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Set all files to use line feed endings (since we can't match only ones without an extension)
|
||||
* eol=lf
|
||||
# And then reset all the files with extensions back to default
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
|
||||
# lf for the docs as it's auto-generated and will otherwise trigger an uncommited error on windows
|
||||
doc/cli.markdown text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
||||
# Prevent auto merging of the npm-shrinkwrap.json file: see notes in CONTRIBUTING.md
|
||||
/npm-shrinkwrap.json merge=binary
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@ -0,0 +1 @@
|
||||
* @CameronDiver @pdcastro @srlowe @thgreasi
|
77
.github/ISSUE_TEMPLATE.md
vendored
77
.github/ISSUE_TEMPLATE.md
vendored
@ -1,2 +1,75 @@
|
||||
- **resin-cli version:**
|
||||
- **Operating system and architecture:**
|
||||
|
||||
# About this issue tracker
|
||||
|
||||
*The balena CLI (Command Line Interface) is a tool used to interact with the balena platform.
|
||||
This GitHub issue tracker is used for bug reports and feature requests regarding the CLI
|
||||
tool. General and troubleshooting questions (such as setting up your project to work with a
|
||||
balenalib base image) are encouraged to be posted to the [balena
|
||||
forums](https://forums.balena.io), which are monitored by balena's support team and where the
|
||||
community can both contribute and benefit from the answers.*
|
||||
|
||||
*Please also check that this issue is not a duplicate. If there is another issue describing
|
||||
the same problem or feature please add comments to the existing issue.*
|
||||
|
||||
*Thank you for your time and effort creating the issue report, and helping us improve the
|
||||
balena CLI!*
|
||||
|
||||
---
|
||||
|
||||
# Expected Behavior
|
||||
|
||||
Please describe what you were expecting to happen. If applicable, please add links to
|
||||
documentation you were following, or to projects that you were trying to push/build.
|
||||
|
||||
# Actual Behavior
|
||||
|
||||
Please describe what actually happened instead:
|
||||
* Quoting logs and error message is useful. If possible, quote the **full** output of the
|
||||
CLI, not just the error message.
|
||||
* Please quote the **full command line** too. Sometimes users report that they were
|
||||
"pushing" or "building" a project, but there are several ways to do so and several
|
||||
possible "targets" such as balenaCloud, openBalena, local balenaOS device, etc.
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myApp
|
||||
balena push 192.168.0.12
|
||||
balena deploy myApp
|
||||
balena deploy myApp --build
|
||||
balena build . -a myApp
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
Each of the above command lines executes different code behind the scenes, so quoting the
|
||||
full command line is very helpful.
|
||||
|
||||
Running the CLI in debug mode (`--debug` flag or `DEBUG=1` environment variable) may reveal
|
||||
additional information. The `--logs` option reveals additional information for the commands:
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myApp --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
||||
This is the most important and helpful part of a bug report. If we cannot reproduce the
|
||||
problem, it is difficult to tell what the fix should be, or whether code changes have
|
||||
fixed it.
|
||||
|
||||
1.
|
||||
1.
|
||||
1.
|
||||
|
||||
# Specifications
|
||||
|
||||
- **balena CLI version:** e.g. 1.2.3 (output of the `"balena version -a"` command)
|
||||
- **Operating system version:** e.g. Windows 10, Ubuntu 18.04, macOS 10.14.5
|
||||
- **32/64 bit OS and processor:** e.g. 32-bit Windows on 64-bit Intel processor
|
||||
- **Install method:** npm or zip package or executable installer
|
||||
- **If npm install, Node.js and npm version:** e.g. Node v8.16.0 and npm v6.4.1
|
||||
|
||||
# Additional References
|
||||
|
||||
If applicable, please add additional links to GitHub projects, forums.balena.io threads,
|
||||
gist.github.com, Google Drive attachments, etc.
|
||||
|
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
<!-- You can remove tags that do not apply. -->
|
||||
Resolves: # <!-- Refer an issue of this repository that this PR fixes -->
|
||||
Change-type: major|minor|patch <!-- See https://semver.org/ -->
|
||||
Depends-on: <url> <!-- This change depends on a PR to get merged/deployed first -->
|
||||
See: <url> <!-- Refer to any external resource, like a PR, document or discussion -->
|
||||
|
||||
---
|
||||
Please check the CONTRIBUTING.md file for relevant information and some
|
||||
guidance. Keep in mind that the CLI is a cross-platform application that runs
|
||||
on Windows, macOS and Linux. Tests will be automatically run by balena CI on
|
||||
all three operating systems, but this will only help if you have added test
|
||||
code that exercises the modified or added feature code.
|
||||
|
||||
Note that each commit message (currently only the first line) will be
|
||||
automatically copied to the CHANGELOG.md file, so try writing it in a way
|
||||
that describes the feature or fix for CLI users.
|
||||
|
||||
If there isn't a linked issue or if the linked issue doesn't quite match the
|
||||
PR, please add a PR description to explain its purpose or the features that it
|
||||
implements. Adding PR comments to blocks of code that aren't self explanatory
|
||||
usually helps with the review process.
|
||||
|
||||
If the PR introduces security considerations or affects the development, build
|
||||
or release process, please be sure to highlight this in the PR description.
|
||||
|
||||
Thank you very much for your contribution!
|
15
.gitignore
vendored
15
.gitignore
vendored
@ -12,6 +12,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
@ -24,14 +25,22 @@ build/Release
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
|
||||
node_modules
|
||||
|
||||
npm-shrinkwrap.json
|
||||
package-lock.json
|
||||
.resinconf
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
|
@ -1,5 +1,2 @@
|
||||
coffee_script:
|
||||
config_file: coffeelint.json
|
||||
|
||||
javascript:
|
||||
enabled: false
|
||||
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"useTabs": true
|
||||
}
|
31
.resinci.yml
Normal file
31
.resinci.yml
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: darwin
|
||||
os: macos
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- name: windows
|
||||
os: windows
|
||||
architecture: x86
|
||||
node_versions:
|
||||
- "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
|
||||
|
8743
.versionbot/CHANGELOG.yml
Normal file
8743
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
2112
CHANGELOG.md
2112
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
226
CONTRIBUTING.md
Normal file
226
CONTRIBUTING.md
Normal file
@ -0,0 +1,226 @@
|
||||
# Contributing
|
||||
|
||||
The balena CLI is an open source project and your contribution is welcome!
|
||||
|
||||
* Install the dependencies listed in the [NPM Installation](./INSTALL.md#npm-installation)
|
||||
section of the `INSTALL.md` file. Check the section [Additional
|
||||
Dependencies](./INSTALL.md#additional-dependencies) too.
|
||||
* Clone the `balena-cli` repository, `cd` to it and run `npm install`.
|
||||
* Build the CLI with `npm run build` or `npm test`, and execute it with `./bin/balena`
|
||||
(on a Windows command prompt, you may need to run `node .\bin\balena`).
|
||||
|
||||
In order to ease development:
|
||||
|
||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
|
||||
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
||||
|
||||
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
||||
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
|
||||
this will only help if you add some test cases for your new code!
|
||||
|
||||
## ./bin/balena-dev and oclif
|
||||
|
||||
When using `./bin/balena-dev` with oclif-converted commands, it is currently necessary to manually
|
||||
edit the `oclif` section of `package.json` to replace `./build` with `./lib` as follows:
|
||||
|
||||
Change from:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./build/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./build/hooks/prerun/track"
|
||||
```
|
||||
|
||||
To:
|
||||
```
|
||||
"oclif": {
|
||||
"commands": "./lib/actions-oclif",
|
||||
"hooks": {
|
||||
"prerun": "./lib/hooks/prerun/track"
|
||||
```
|
||||
|
||||
And then remember to change it back before pushing the pull request. This is obviously error prone
|
||||
and inconvenient, and improvement suggestions are welcome: is there a better solution than
|
||||
automatically editing `package.json`? It is doable, if it is what needs to be done.
|
||||
|
||||
## Semantic versioning and commit messages
|
||||
|
||||
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) specifically requires [MSYS2](https://www.msys2.org/) to be installed. Other than that,
|
||||
the standard Command Prompt or PowerShell can be used (though MSYS2 is still handy, as it provides
|
||||
'git' and a number of common unix utilities). If you make changes to `package.json` scripts, check
|
||||
they also run on a standard Windows Command Prompt.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
The `npm-shrinkwrap.json` file is used to control package dependencies, as documented at
|
||||
https://docs.npmjs.com/files/shrinkwrap.json.
|
||||
|
||||
While developing, the `package.json` file is often modified by, or before, running `npm install`
|
||||
in order to add, remove or modify dependencies. When `npm install` is executed, it automatically
|
||||
updates the `npm-shrinkwrap.json` file as well, **taking into account not only the `package.json`
|
||||
file but also the current state of the `node_modules` folder in your computer.**
|
||||
|
||||
Meanwhile, as a text (JSON) file, `git` is capable of merging the `npm-shrinkwrap.json` file during
|
||||
operations like `rebase`, `cherry-pick` and `pull`. But git's automated merge is not the
|
||||
recommended way of updating the `npm-shrinkwrap.json` file, because it does not take into account
|
||||
duplicates or conflicts in the dependency tree, or indeed the state of the `package.json` file
|
||||
(which may have just been merged). In extreme cases, the automated merge may actually result in a
|
||||
broken installation. For these reasons, automatic merging of the `npm-shrinkwrap.json` was disabled
|
||||
through the `.gitattributes` file (the "binary merge driver" allows diff'ing but prevents automatic
|
||||
merging). Operations like `git rebase` may then result in an error like:
|
||||
|
||||
```text
|
||||
$ git rebase master
|
||||
warning: Cannot merge binary files: npm-shrinkwrap.json (HEAD vs. c34942b9... test)
|
||||
Auto-merging npm-shrinkwrap.json
|
||||
CONFLICT (content): Merge conflict in npm-shrinkwrap.json
|
||||
error: Failed to merge in the changes.
|
||||
```
|
||||
|
||||
Whether or not there is a merge error, the following commands are the recommended way of updating
|
||||
and committing the `npm-shrinkwrap.json` file:
|
||||
|
||||
```bash
|
||||
$ rm -rf node_modules # Linux / Mac
|
||||
$ rmdir /s node_modules # Windows Command Prompt
|
||||
$ npm checkout master -- npm-shrinkwrap.json # revert it to the master branch state
|
||||
$ npm install # "cleanly" update the npm-shrinkwrap.json file
|
||||
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
[TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
|
||||
Typescript, in order to take advantage of static typing and formal programming interfaces.
|
||||
The migration towards Typescript is taking place gradually, as part of maintenance work or
|
||||
the implementation of new features. Historically, the CLI was originally written in
|
||||
[CoffeeScript](https://coffeescript.org), but all CoffeeScript code was migrated to either
|
||||
Javascript or Typescript.
|
||||
|
||||
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
|
||||
framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
as native installers for Windows, macOS and Linux, and support for custom flag parsing (for
|
||||
example, we're still battling with Capitano's behavior of dropping leading zeros of arguments that
|
||||
look like integers, such as some abbreviated UUIDs). Again, the migration is taking place
|
||||
gradually, with some CLI commands parsed by oclif and others by Capitano. A simple command line
|
||||
pre-parsing takes place in `preparser.ts`, to decide whether to route full parsing to Capitano or
|
||||
to oclif.
|
||||
|
||||
## Programming style
|
||||
|
||||
`npm run build` also runs [balena-lint](https://www.npmjs.com/package/@balena/lint), which automatically
|
||||
reformats the code. Beyond that, we have a preference for Javascript promises over callbacks, and for
|
||||
`async/await` over `.then()`.
|
||||
|
||||
## Updating upstream dependencies
|
||||
|
||||
In order to get proper nested changelogs, when updating upstream modules that are in the repo.yml
|
||||
(like the balena-sdk), the commit body has to contain a line with the following format:
|
||||
```
|
||||
Update balena-sdk from 12.0.0 to 12.1.0
|
||||
```
|
||||
|
||||
Since this is error prone, it's suggested to use the following npm script:
|
||||
```
|
||||
npm run update balena-sdk ^12.1.0
|
||||
```
|
||||
|
||||
This will create a new branch (only if you are currently on master), run `npm update` with the
|
||||
version you provided as a target and commit the package.json & npm-shrinkwrap.json. The script by
|
||||
default will set the `Change-type` to `patch` or `minor`, depending on the semver change of the
|
||||
updated dependency, but if you need to use a different one (eg `major`) you can specify it as an
|
||||
extra argument:
|
||||
```
|
||||
npm run update balena-sdk ^12.14.0 patch
|
||||
npm run update balena-sdk ^13.0.0 major
|
||||
```
|
||||
|
||||
## Common gotchas
|
||||
|
||||
One thing that most CLI bugs have in common is the absence of test cases exercising the broken
|
||||
code, so writing some test code is a great idea. Having said that, there are also some common
|
||||
gotchas to bear in mind:
|
||||
|
||||
* Forward slashes ('/') _vs._ backslashes ('\') in file paths. The Node.js
|
||||
[path.sep](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_sep) variable stores a
|
||||
platform-specific path separator character: the backslash on Windows and the forward slash on
|
||||
Linux and macOS. The
|
||||
[path.join](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_join_paths) function
|
||||
builds paths using such platform-specific path separator. However:
|
||||
* Note that Windows (kernel, cmd.exe, PowerShell, many applications) accepts ***both*** forward
|
||||
slashes and backslashes as path separators (including mixing them in a path string), so code
|
||||
like `mypath.split(path.sep)` may fail on Windows if `mypath` contains forward slashes. The
|
||||
[path.parse](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_parse_path) function
|
||||
understands both forward slashes and backslashes on Windows, and the
|
||||
[path.normalize](https://nodejs.org/docs/latest-v12.x/api/path.html#path_path_normalize_path)
|
||||
function will _replace_ forward slashes with backslashes.
|
||||
* In [tar](https://en.wikipedia.org/wiki/Tar_(computing)#File_format) streams sent to the Docker
|
||||
daemon and to balenaCloud, the forward slash is the only acceptable path separator, regardless
|
||||
of the OS where the CLI is running. Therefore, `path.sep` and `path.join` should never be used
|
||||
when handling paths in tar streams! `path.posix.join` may be used instead of `path.join`.
|
||||
|
||||
* Avoid using the system shell to execute external commands, for example:
|
||||
`child_process.exec('ssh "arg1" "arg2"');`
|
||||
`child_process.spawn('ssh "arg1" "arg2"', { shell: true });`
|
||||
Besides the usual security concerns of unsanitized strings, another problem is to get argument
|
||||
escaping right because of the differences between the Windows 'cmd.exe' shell and the Unix
|
||||
'/bin/sh'. For example, 'cmd.exe' doesn't recognize single quotes like '/bin/sh', and uses the
|
||||
caret (^) instead of the backslash as the escape character. Bug territory! Most of the time,
|
||||
it is possible to avoid relying on the shell altogether by providing a Javascript array of
|
||||
arguments:
|
||||
`spawn('ssh', ['arg1', 'arg2'], { shell: false});`
|
||||
To allow for logging and debugging, the [which](https://www.npmjs.com/package/which) package may
|
||||
be used to get the full path of a command before executing it, without relying on any shell:
|
||||
`const fullPath = await which('ssh');`
|
||||
`console.log(fullPath); # 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'`
|
||||
`spawn(fullPath, ['arg1', 'arg2'], { shell: false });`
|
||||
|
||||
* Avoid the `instanceof` operator when testing against classes/types from external packages
|
||||
(including base classes), because `npm install` may result in multiple versions of the same
|
||||
package being installed (to satisfy declared dependencies) and a false negative may result when
|
||||
comparing an object instance from one package version with a class of another package version
|
||||
(even if the implementations are identical in both packages). For example, once we fixed a bug
|
||||
where the test:
|
||||
`error instanceof BalenaApplicationNotFound`
|
||||
changed from true to false because `npm install` added an additional copy of the `balena-errors`
|
||||
package to satisfy a minor `balena-sdk` version update:
|
||||
`$ find node_modules -name balena-errors`
|
||||
`node_modules/balena-errors`
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
231
INSTALL.md
Normal file
231
INSTALL.md
Normal file
@ -0,0 +1,231 @@
|
||||
# balena CLI Installation Instructions
|
||||
|
||||
There are 3 options to choose from to install balena's CLI:
|
||||
|
||||
* [Executable Installer](#executable-installer): the easiest method on Windows and macOS, using the
|
||||
traditional graphical desktop application installers.
|
||||
* [Standalone Zip Package](#standalone-zip-package): these are plain zip files with the balena CLI
|
||||
executable in them: extract and run. Available for all platforms: Linux, Windows, macOS.
|
||||
Recommended also for scripted installation in CI (continuous integration) environments.
|
||||
* [NPM Installation](#npm-installation): recommended for Node.js developers who may be interested
|
||||
in integrating the balena CLI in their existing projects or workflow.
|
||||
|
||||
Some specific CLI commands have a few extra installation steps: see section [Additional
|
||||
Dependencies](#additional-dependencies).
|
||||
|
||||
> **Windows users:**
|
||||
> * There is a [YouTube video tutorial](https://www.youtube.com/watch?v=2LApclXFqsg) for installing
|
||||
> and getting started with the balena CLI on Windows. (The video uses the standalone zip package
|
||||
> option.)
|
||||
> * If you are using Microsoft's [Windows Subsystem for
|
||||
> Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL), install a balena CLI release
|
||||
> for Linux rather than for Windows, like the standalone zip package for Linux. An installation
|
||||
> with the graphical executable installer for Windows will **not** work with WSL.
|
||||
|
||||
## Executable Installer
|
||||
|
||||
Recommended for Windows (but not Windows Subsystem for Linux) and macOS:
|
||||
|
||||
1. Download the latest installer from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with "-installer", for example:
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer.
|
||||
_If you are using macOS Catalina (10.15), [check this known issue and
|
||||
workaround](https://github.com/balena-io/balena-cli/issues/1479)._
|
||||
|
||||
3. After the installation completes, close and re-open any open [command
|
||||
terminal](https://www.balena.io/docs/reference/cli/#choosing-a-shell-command-promptterminal)
|
||||
windows so that the changes made by the installer to the PATH environment variable can take
|
||||
effect. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
|
||||
* `balena version` - should print the installed CLI version
|
||||
* `balena help` - should print the balena CLI help
|
||||
|
||||
> Note: If you had previously installed the CLI using a standalone zip package, it may be a good
|
||||
> idea to check your system's `PATH` environment variable for duplicate entries, as the terminal
|
||||
> will use the entry that comes first. Check the [Standalone Zip Package](#standalone-zip-package)
|
||||
> instructions for how to modify the PATH variable.
|
||||
|
||||
By default, the CLI is installed to the following folders:
|
||||
|
||||
OS | Folders
|
||||
--- | ---
|
||||
Windows: | `C:\Program Files\balena-cli\`
|
||||
macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
|
||||
## Standalone Zip Package
|
||||
|
||||
1. Download the latest zip file from the [releases page](https://github.com/balena-io/balena-cli/releases).
|
||||
Look for a file name that ends with the word "standalone", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip` ← _also for the Windows Subsystem for Linux_
|
||||
`balena-cli-vX.Y.Z-macOS-x64-standalone.zip`
|
||||
`balena-cli-vX.Y.Z-windows-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable.
|
||||
See instructions for:
|
||||
[Linux](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix) |
|
||||
[macOS](https://www.architectryan.com/2012/10/02/add-to-the-path-on-mac-os-x-mountain-lion/#.Uydjga1dXDg) |
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS Catalina (10.15), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> It should however work with all "desktop" or "server" distributions, e.g. Ubuntu, Debian, Suse,
|
||||
> Fedora, Arch Linux and many more.
|
||||
> * Note that moving the `balena` executable out of the extracted `balena-cli` folder on its own
|
||||
> (e.g. moving it to `/usr/local/bin/balena`) will **not** work, as it depends on the other
|
||||
> folders and files also present in the `balena-cli` folder.
|
||||
|
||||
To update the CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## NPM Installation
|
||||
|
||||
If you are a Node.js developer, you may wish to install the balena CLI via [npm](https://www.npmjs.com).
|
||||
The npm installation involves building native (platform-specific) binary modules, which require
|
||||
some additional development tools to be installed first:
|
||||
|
||||
* [Node.js](https://nodejs.org/) version 10 or 12 (version 14 is not yet fully supported)
|
||||
* **Linux, macOS** and **Windows Subsystem for Linux (WSL):**
|
||||
Installing Node via [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) is recommended.
|
||||
When the "system" or "default" Node.js and npm packages are installed with "apt-get" in Linux
|
||||
distributions like Ubuntu, users often report permission or compilation errors when running
|
||||
"npm install". This [sample
|
||||
Dockerfile](https://gist.github.com/pdcastro/5d4d96652181e7da685a32caf629dd44) shows the CLI
|
||||
installation steps on an Ubuntu 18.04 base image.
|
||||
* [Python 2.7](https://www.python.org/), [git](https://git-scm.com/), [make](https://www.gnu.org/software/make/), [g++](https://gcc.gnu.org/)
|
||||
* **Linux** and **Windows Subsystem for Linux (WSL):**
|
||||
`sudo apt-get install -y python git make g++`
|
||||
* **macOS:** install Apple's Command Line Tools by running on a Terminal window:
|
||||
`xcode-select --install`
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
|
||||
* Node.js from the [Nodejs.org download page](https://nodejs.org/en/download/).
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++`, `ssh`, `rsync`
|
||||
and more:
|
||||
* `pacman -S git openssh rsync gcc make`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
|
||||
interactive CLI menus to misbehave. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
|
||||
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
|
||||
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (which
|
||||
provides Python 2.7 and more), by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
|
||||
`npm install -g --production windows-build-tools`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --production --unsafe-perm
|
||||
```
|
||||
|
||||
`--unsafe-perm` is required when `npm install` is executed as the root user, or on systems where
|
||||
the global install directory is not user-writable. It allows npm install steps to download and save
|
||||
prebuilt native binaries, and also allows the execution of npm scripts like `postinstall` that are
|
||||
used to patch dependencies. It is usually possible to omit `--unsafe-perm` if installing under a
|
||||
regular (non-root) user account, especially if using a user-managed node installation such as
|
||||
[nvm](https://github.com/creationix/nvm).
|
||||
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
* The `balena ssh` command requires a recent version of the `ssh` command-line tool to be available:
|
||||
* macOS and Linux usually already have it installed. Otherwise, search for the available packages
|
||||
on your specific Linux distribution, or for the Mac consider the [Xcode command-line
|
||||
tools](https://developer.apple.com/xcode/features/) or [homebrew](https://brew.sh/).
|
||||
|
||||
* Microsoft started distributing an SSH client with Windows 10, which we understand is
|
||||
automatically installed through Windows Update, but can be manually installed too
|
||||
([more information](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)).
|
||||
For other versions of Windows, there are several ssh/OpenSSH clients provided by 3rd parties.
|
||||
|
||||
* The [`proxytunnel`](http://proxytunnel.sourceforge.net/) package (command-line tool) is needed
|
||||
for the `balena ssh` command to work behind a proxy. It is available for Linux distributions
|
||||
like Ubuntu/Debian (`apt install proxytunnel`), and for macOS through
|
||||
[Homebrew](https://brew.sh/). Windows support is limited to the Windows Subsystem for Linux
|
||||
(e.g., by installing Ubuntu through the Microsoft App Store). Check the
|
||||
[README](https://github.com/balena-io/balena-cli/blob/master/README.md) file for proxy
|
||||
configuration instructions.
|
||||
|
||||
* The `balena preload`, `balena build` and `balena deploy --build` commands require
|
||||
[Docker](https://docs.docker.com/install/overview/) or [balenaEngine](https://www.balena.io/engine/)
|
||||
to be available:
|
||||
* The `balena preload` command requires the Docker Engine to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Docker Desktop for Mac and
|
||||
Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1, so
|
||||
the workaround is to downgrade to version 18.06.1 (links: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
and [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)).
|
||||
See more details in [CLI issue 1099](https://github.com/balena-io/balena-cli/issues/1099).
|
||||
* Commonly, Docker is installed on the same machine where the CLI is being used, but the
|
||||
`balena build` and `balena deploy` commands can also use a remote Docker Engine (daemon)
|
||||
or balenaEngine (which could be a remote device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)) by specifying
|
||||
its IP address and port number as command-line options. Check the documentation for each
|
||||
command, e.g. `balena help build`, or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
* If you are using Microsoft's [Windows Subsystem for
|
||||
Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL) and Docker Desktop for
|
||||
Windows, check the [FAQ item "Docker seems to be
|
||||
unavailable"](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md#docker-seems-to-be-unavailable-error-when-using-windows-subsystem-for-linux-wsl).
|
||||
|
||||
* The `balena scan` command requires a multicast DNS (mDNS) service like Bonjour or Avahi:
|
||||
* On Windows, check if 'Bonjour' is installed (Control Panel > Programs and Features).
|
||||
If not, you can download Bonjour for Windows from https://support.apple.com/kb/DL999
|
||||
* Most 'desktop' Linux distributions ship with [Avahi](https://en.wikipedia.org/wiki/Avahi_(software)).
|
||||
Search for the installation command for your distribution. E.g. for Ubuntu:
|
||||
`sudo apt-get install avahi-daemon`
|
||||
* macOS comes with [Bonjour](https://en.wikipedia.org/wiki/Bonjour_(software)) built-in.
|
||||
|
||||
* The `balena os configure` command is currently not supported on Windows natively. Windows users are advised
|
||||
to install the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL)
|
||||
with Ubuntu, and use the Linux release of the balena CLI.
|
||||
|
||||
|
||||
## 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.
|
202
README.md
202
README.md
@ -1,98 +1,168 @@
|
||||
Resin CLI
|
||||
=========
|
||||
# balena CLI
|
||||
|
||||
> The official resin.io CLI tool.
|
||||
The official balena CLI tool.
|
||||
|
||||
[](http://badge.fury.io/js/resin-cli)
|
||||
[](https://david-dm.org/resin-io/resin-cli)
|
||||
[](https://gitter.im/resin-io/chat)
|
||||
[](http://badge.fury.io/js/balena-cli)
|
||||
[](https://david-dm.org/balena-io/balena-cli)
|
||||
|
||||
Requisites
|
||||
----------
|
||||
## About
|
||||
|
||||
- [NodeJS](https://nodejs.org) (>= v4)
|
||||
- [Git](https://git-scm.com)
|
||||
- The following executables should be correctly installed in your shell environment:
|
||||
- `ssh`: Any recent version of the OpenSSH ssh client (required by `resin sync` and `resin ssh`)
|
||||
- if you need `ssh` to work behind the proxy you also need [`proxytunnel`](http://proxytunnel.sourceforge.net/) installed (available as `proxytunnel` package for Ubuntu, for example)
|
||||
- `rsync`: >= 2.6.9 (required by `resin sync`)
|
||||
The balena CLI (Command-Line Interface) allows you to interact with the balenaCloud and the
|
||||
[balena API](https://www.balena.io/docs/reference/api/overview/) through a terminal window
|
||||
on Linux, macOS or Windows. You can also write shell scripts around it, or import its Node.js
|
||||
modules to use it programmatically.
|
||||
As an [open-source project on GitHub](https://github.com/balena-io/balena-cli/), your contribution
|
||||
is also welcome!
|
||||
|
||||
##### Windows Support
|
||||
## Installation
|
||||
|
||||
Before installing resin-cli, you'll need a working node-gyp environment. If you don't already have one you'll see native module build errors during installation. To fix this, run `npm install -g --production windows-build-tools` in an administrator console (available as 'Command Prompt (Admin)' when pressing windows+x in Windows 7+).
|
||||
Check the [balena CLI installation instructions on GitHub](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md).
|
||||
|
||||
`resin sync` and `resin ssh` have not been thoroughly tested on the standard Windows cmd.exe shell. We recommend using bash (or a similar) shell, like Bash for Windows 10 or [Git for Windows](https://git-for-windows.github.io/).
|
||||
## Getting Started
|
||||
|
||||
If you still want to use `cmd.exe` you will have to use a package manager like MinGW or chocolatey. For MinGW the steps are:
|
||||
### Choosing a shell (command prompt/terminal)
|
||||
|
||||
1. Install [MinGW](http://www.mingw.org).
|
||||
2. Install the `msys-rsync` and `msys-openssh` packages.
|
||||
3. Add MinGW to the `%PATH%` if this hasn't been done by the installer already. The location where the binaries are places is usually `C:\MinGW\msys\1.0\bin`, but it can vary if you selected a different location in the installer.
|
||||
4. Copy your SSH keys to `%homedrive%%homepath\.ssh`.
|
||||
5. If you need `ssh` to work behind the proxy you also need to install [proxytunnel](http://proxytunnel.sourceforge.net/)
|
||||
On **Windows,** the standard Command Prompt (`cmd.exe`) and
|
||||
[PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6)
|
||||
are supported. We are aware of users also having a good experience with alternative shells,
|
||||
including:
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git openssh rsync`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
|
||||
menus to break. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
|
||||
actually uses MSYS2). For a screenshot, check this
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** is recommended. See
|
||||
[FAQ](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md) for using balena
|
||||
CLI with WSL and Docker Desktop for Windows.
|
||||
|
||||
### Install
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. _Optionally,_ `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
This might require elevated privileges in some environments.
|
||||
### Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ npm install --global --production resin-cli
|
||||
$ balena login
|
||||
```
|
||||
|
||||
### Login
|
||||
### Proxy support
|
||||
|
||||
```sh
|
||||
$ resin login
|
||||
HTTP(S) proxies can be configured through any of the following methods, in precedence order
|
||||
(from higher to lower):
|
||||
|
||||
* The `BALENARC_PROXY` environment variable in URL format, with protocol (`http` or `https`),
|
||||
host, port and optionally basic auth. Examples:
|
||||
* `export BALENARC_PROXY='https://bob:secret@proxy.company.com:12345'`
|
||||
* `export BALENARC_PROXY='http://localhost:8000'`
|
||||
|
||||
* The `proxy` setting in the [CLI config
|
||||
file](https://www.npmjs.com/package/balena-settings-client#documentation). It may be:
|
||||
* A string in URL format, e.g. `proxy: 'http://localhost:8000'`
|
||||
* An object in the format:
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
protocol: 'http'
|
||||
host: 'proxy.company.com'
|
||||
port: 12345
|
||||
proxyAuth: 'bob:secret'
|
||||
```
|
||||
|
||||
* The `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables, in the same URL format as
|
||||
`BALENARC_PROXY`.
|
||||
|
||||
> Note: The `balena ssh` command has additional setup requirements to work behind a proxy.
|
||||
> Check the [installation instructions](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md),
|
||||
> and ensure that the proxy server is configured to allow proxy requests to ssh port 22, using
|
||||
> SSL encryption. For example, in the case of the [Squid](http://www.squid-cache.org/) proxy
|
||||
> server, it should be configured with the following rules in the `squid.conf` file:
|
||||
> `acl SSL_ports port 22`
|
||||
> `acl Safe_ports port 22`
|
||||
|
||||
#### Proxy exclusion
|
||||
|
||||
The `BALENARC_NO_PROXY` variable may be used to exclude specified destinations from proxying.
|
||||
|
||||
> * This feature requires balena CLI version 11.30.8 or later. In the case of the npm [installation
|
||||
> option](https://github.com/balena-io/balena-cli/blob/master/INSTALL.md), it also requires
|
||||
> Node.js version 10.16.0 or later.
|
||||
> * To exclude a `balena ssh` target from proxying (IP address or `.local` hostname), the
|
||||
> `--noproxy` option should be specified in addition to the `BALENARC_NO_PROXY` variable.
|
||||
|
||||
By default (if `BALENARC_NO_PROXY` is not defined), all [private IPv4
|
||||
addresses](https://en.wikipedia.org/wiki/Private_network) and `'*.local'` hostnames are excluded
|
||||
from proxying. Other hostnames that resolve to private IPv4 addresses are **not** excluded by
|
||||
default, because matching takes place before name resolution.
|
||||
|
||||
`localhost` and `127.0.0.1` are always excluded from proxying, regardless of the value of
|
||||
BALENARC_NO_PROXY.
|
||||
|
||||
The format of the `BALENARC_NO_PROXY` environment variable is a comma-separated list of patterns
|
||||
that are matched against hostnames or IP addresses. For example:
|
||||
|
||||
```
|
||||
export BALENARC_NO_PROXY='*.local,dev*.mycompany.com,192.168.*'
|
||||
```
|
||||
|
||||
_(Typically useful, but not strictly required for all commands)_
|
||||
Matched patterns are excluded from proxying. Wildcard expressions are documented at
|
||||
[matcher](https://www.npmjs.com/package/matcher#usage). Matching takes place _before_ name
|
||||
resolution, so a pattern like `'192.168.*'` will **not** match a hostname that resolves to an IP
|
||||
address like `192.168.1.2`.
|
||||
|
||||
### Run commands
|
||||
## Command reference documentation
|
||||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
---
|
||||
## Support, FAQ and troubleshooting
|
||||
|
||||
Plugins
|
||||
-------
|
||||
If you come across any problems or would like to get in touch:
|
||||
|
||||
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!
|
||||
* 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/).
|
||||
|
||||
FAQ
|
||||
---
|
||||
## Deprecation policy
|
||||
|
||||
### Where is my configuration file?
|
||||
The balena CLI uses [semver versioning](https://semver.org/), with the concepts
|
||||
of major, minor and patch version releases.
|
||||
|
||||
The per-user configuration file lives in `$HOME/.resinrc.yml` or `%UserProfile%\_resinrc.yml`, in Unix based operating systems and Windows respectively.
|
||||
The latest release of the previous major version of the balena CLI will remain
|
||||
compatible with the balenaCloud backend services for one year from the date when
|
||||
the next major version is released. For example, balena CLI v10.17.5, as the
|
||||
latest v10 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v11.0.0 is released.
|
||||
|
||||
The Resin CLI also attempts to read a `resinrc.yml` file in the current directory, which takes precedence over the per-user configuration file.
|
||||
At the end of this period, the older major version is considered deprecated and
|
||||
some of the functionality that depends on balenaCloud services may stop working
|
||||
at any time.
|
||||
Users are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
### How do I point the Resin CLI to staging?
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
The easiest way is to set the `RESINRC_RESIN_URL=resinstaging.io` environment variable.
|
||||
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!
|
||||
|
||||
Alternatively, you can edit your configuration file and set `resinUrl: resinstaging.io` to persist this setting.
|
||||
## License
|
||||
|
||||
### How do I make the Resin CLI persist data in another directory?
|
||||
|
||||
The Resin CLI persists your session token, as well as cached images in `$HOME/.resin` or `%UserProfile%\_resin`.
|
||||
|
||||
Pointing the Resin CLI to persist data in another location is necessary in certain environments, like a server, where there is no home directory, or a device running resinOS, which erases all data after a restart.
|
||||
|
||||
You can accomplish this by setting `RESINRC_DATA_DIRECTORY=/opt/resin` or adding `dataDirectory: /opt/resin` to your configuration file, replacing `/opt/resin` with your desired directory.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
If you're having any problems, check our [troubleshooting guide](https://github.com/resin-io/resin-cli/blob/master/TROUBLESHOOTING.md) and if your problem is not addressed there, please [raise an issue](https://github.com/resin-io/resin-cli/issues/new) on GitHub and the resin.io team will be happy to help.
|
||||
|
||||
You can also get in touch with us in the resin.io [forums](https://forums.resin.io/).
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The project is licensed under the Apache 2.0 license.
|
||||
The project is licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
A copy is also available in the LICENSE file in this repository.
|
||||
|
@ -1,15 +1,41 @@
|
||||
Troubleshooting
|
||||
===============
|
||||
# FAQ & Troubleshooting
|
||||
|
||||
This document contains common issues related to the Resin CLI, and how to fix them.
|
||||
This document contains some common issues, questions and answers related to the balena CLI.
|
||||
|
||||
### After burning to an sdcard, my device doesn't boot
|
||||
## Where is my configuration file?
|
||||
|
||||
The per-user configuration file lives in `$HOME/.balenarc.yml` or `%UserProfile%\_balenarc.yml`, in
|
||||
Unix based operating systems and Windows respectively.
|
||||
|
||||
The balena CLI also attempts to read a `balenarc.yml` file in the current directory, which takes
|
||||
precedence over the per-user configuration file.
|
||||
|
||||
## How do I point the balena CLI to staging?
|
||||
|
||||
The easiest way is to set the `BALENARC_BALENA_URL=balena-staging.com` environment variable.
|
||||
|
||||
Alternatively, you can edit your configuration file and set `balenaUrl: balena-staging.com` to
|
||||
persist this setting.
|
||||
|
||||
## How do I make the balena CLI persist data in another directory?
|
||||
|
||||
The balena CLI persists your session token, as well as cached images in `$HOME/.balena` or
|
||||
`%UserProfile%\_balena`.
|
||||
|
||||
Pointing the balena CLI to persist data in another location is necessary in certain environments,
|
||||
like a server, where there is no home directory, or a device running balenaOS, which erases all
|
||||
data after a restart.
|
||||
|
||||
You can accomplish this by setting `BALENARC_DATA_DIRECTORY=/opt/balena` or adding `dataDirectory:
|
||||
/opt/balena` to your configuration file, replacing `/opt/balena` with your desired directory.
|
||||
|
||||
## After burning to an sdcard, my device doesn't boot
|
||||
|
||||
- The downloaded image is not complete (download was interrupted).
|
||||
|
||||
Please clean the cache (`%HOME/.resin/cache` or `C:\Users\<user>\_resin\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
Please clean the cache (`%HOME/.balena/cache` or `C:\Users\<user>\_balena\cache`) and run the command again. In the future, the CLI will check that the image is not complete and clean the cache for you.
|
||||
|
||||
### I get a permission error when burning to an sdcard
|
||||
## I get a permission error when burning to an sdcard
|
||||
|
||||
- The SDCard is locked.
|
||||
|
||||
@ -24,36 +50,79 @@ net.js:156
|
||||
Error: EINVAL, invalid argument
|
||||
at new Socket (net.js:156:18)
|
||||
at process.stdin (node.js:664:19)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\resin-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
at Object.Interface.createInterface (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\node_modules\readline2\index.js:31:43)
|
||||
at PromptUI.UI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\baseUI.js:23:40)
|
||||
at new PromptUI (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\ui\prompt.js:26:8)
|
||||
at Object.promptModule [as prompt] (C:\cygwin\home\Juan Cruz Viotti\Projects\balena-cli\node_modules\inquirer\lib\inquirer.js:27:14)
|
||||
```
|
||||
|
||||
- Some interactive widgets don't work on `Cygwin`. If you're running Windows, it's preferrable that you use `cmd.exe`, as `Cygwin` is [not official supported by Node.js](https://github.com/chjj/blessed/issues/56#issuecomment-42671945).
|
||||
|
||||
### I get `Invalid MBR boot signature` when configuring a device
|
||||
## I get `Invalid MBR boot signature` when configuring a device
|
||||
|
||||
This error, accompanied with something like: `Expected 0xAA55, but saw 0x29FE` usually indicates a corrupted device operating system image in the cache, due to bad a internet connection during the download process.
|
||||
|
||||
Try clearing the cache with the following command and trying again:
|
||||
|
||||
```sh
|
||||
$ rm -rf $HOME/.resin/cache
|
||||
$ rm -rf $HOME/.balena/cache
|
||||
```
|
||||
|
||||
Or in Windows:
|
||||
|
||||
```sh
|
||||
> del /s /q %UserProfile%\_resin\cache
|
||||
> del /s /q %UserProfile%\_balena\cache
|
||||
```
|
||||
|
||||
### I get `EACCES: permission denied` when logging in
|
||||
## I get `EACCES: permission denied` when logging in
|
||||
|
||||
The Resin CLI stores the session token in `$HOME/.resin` or `C:\Users\<user>\_resin` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the Resin CLI as `root`, and thus the directory got owned by him.
|
||||
The balena CLI stores the session token in `$HOME/.balena` or `C:\Users\<user>\_balena` in UNIX based operating systems and Windows respectively. This error usually indicates that the user doesn't have permissions over that directory, which can happen if you ran the balena CLI as `root`, and thus the directory got owned by him.
|
||||
|
||||
Try resetting the ownership by running:
|
||||
|
||||
```sh
|
||||
$ sudo chown -R <user> $HOME/.resin
|
||||
$ sudo chown -R <user> $HOME/.balena
|
||||
```
|
||||
|
||||
## Broken line wrapping / cursor behavior with `balena ssh`
|
||||
|
||||
Users sometimes come across broken line wrapping or cursor behavior in text terminals, for example when long command lines are typed in a `balena ssh` session, or when using text editors like `vim` or `nano`. This is not something specific to the balena CLI, being also a commonly reported issue with standard remote terminal tools like `ssh` or `telnet`. It is often a remote shell configuration issue (files like `/etc/profile`, `~/.bash_profile`, `~/.bash_login`, `~/.profile` and the like), including UTF-8 misconfiguration, the use of unsupported ASCII control characters in shell prompt formatting (e.g. the `$PS1` env var) or the output of tools or log files that use colored text. The issue can sometimes be fixed by resizing the client terminal window, or by running one or more of the following commands on the shell:
|
||||
|
||||
```sh
|
||||
export TERMINAL=linux
|
||||
stty sane
|
||||
shopt -s checkwinsize
|
||||
bind 'set horizontal-scroll-mode off'
|
||||
```
|
||||
|
||||
Terminal multiplexer tools like GNU `screen` or `tmux` are sometimes reported to fix the issues, though at other times they are reported as the _cause_ of the problem. They have their own configuration files to take into account.
|
||||
|
||||
Further reference:
|
||||
* https://stackoverflow.com/questions/1133031/shell-prompt-line-wrapping-issue
|
||||
* https://superuser.com/questions/46948/any-way-to-fix-screens-mishandling-of-line-wrap-maybe-only-terminal-app
|
||||
* https://unix.stackexchange.com/questions/105958/terminal-prompt-not-wrapping-correctly
|
||||
* https://unix.stackexchange.com/questions/529377/terminal-long-line-wrapping
|
||||
* https://github.com/microsoft/WSL/issues/1436
|
||||
|
||||
If nothing seems to help, consider also using a different client-side terminal application:
|
||||
* Linux: xterm, KDE Konsole, GNOME Terminal
|
||||
* Mac: Terminal, iTerm2
|
||||
* Windows: PowerShell, PuTTY, WSL (Windows Subsystem for Linux)
|
||||
|
||||
## "Docker seems to be unavailable" error when using Windows Subsystem for Linux (WSL)
|
||||
|
||||
When running on WSL, the recommendation is to install a CLI release for Linux, like the standalone
|
||||
zip package for Linux. However, commands like "balena build" that contact a local Docker daemon,
|
||||
like the Docker Desktop for Windows, will try to reach Docker at the Unix socket path
|
||||
`/var/run/docker.sock`, while Docker Desktop for Windows uses a Windows named pipe at
|
||||
`//./pipe/docker_engine` (which the Linux CLI on WSL cannot use). A solution is:
|
||||
|
||||
- Open the Docker Desktop for Windows settings panel and tick the checkbox _"Expose daemon on tcp://localhost:2375 without TLS"._
|
||||
- On the WSL command line, set an env var:
|
||||
`export DOCKER_HOST=tcp://localhost:2375`
|
||||
Alternatively, use the command-line options `-h 127.0.0.1 -p 2375` for commands like `balena build` and `balena deploy`.
|
||||
|
||||
Further reference:
|
||||
|
||||
- https://techcommunity.microsoft.com/t5/Containers/WSL-Interoperability-with-Docker/ba-p/382405
|
||||
- https://forums.docker.com/t/wsl-and-docker-for-windows-cannot-connect-to-the-docker-daemon-at-tcp-localhost-2375-is-the-docker-daemon-running/63571/12
|
||||
|
43
appveyor.yml
Normal file
43
appveyor.yml
Normal file
@ -0,0 +1,43 @@
|
||||
# appveyor file
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
image: Visual Studio 2017
|
||||
|
||||
init:
|
||||
- git config --global core.autocrlf input
|
||||
|
||||
cache:
|
||||
- C:\Users\appveyor\.node-gyp
|
||||
- '%AppData%\npm-cache'
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
|
||||
# what combinations to test
|
||||
environment:
|
||||
matrix:
|
||||
- nodejs_version: 10
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version x64
|
||||
- set PATH=%APPDATA%\npm;%PATH%
|
||||
- npm config set python 'C:\Python27\python.exe'
|
||||
- npm --version
|
||||
# - npm install
|
||||
|
||||
build: off
|
||||
test: off
|
||||
deploy: off
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm test
|
||||
|
||||
deploy_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
# - npm run build:standalone
|
||||
# - npm run build:installer
|
||||
# - IF "%APPVEYOR_REPO_TAG%" == "true" (npm run release)
|
||||
# - IF NOT "%APPVEYOR_REPO_TAG%" == "true" (echo 'Not tagged, skipping deploy')
|
313
automation/build-bin.ts
Normal file
313
automation/build-bin.ts
Normal file
@ -0,0 +1,313 @@
|
||||
/**
|
||||
* @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 type { JsonVersions } from '../lib/actions-oclif/version';
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as filehound from 'filehound';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { exec as execPkg } from 'pkg';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as util from 'util';
|
||||
|
||||
import {
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
|
||||
function dPath(...paths: string[]) {
|
||||
return path.join(ROOT, 'dist', ...paths);
|
||||
}
|
||||
|
||||
interface PathByPlatform {
|
||||
[platform: string]: string;
|
||||
}
|
||||
|
||||
const standaloneZips: PathByPlatform = {
|
||||
linux: dPath(`balena-cli-${version}-linux-${arch}-standalone.zip`),
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-standalone.zip`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-standalone.zip`),
|
||||
};
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
darwin: dPath(`balena-cli-${version}-macOS-${arch}-installer.pkg`),
|
||||
win32: dPath(`balena-cli-${version}-windows-${arch}-installer.exe`),
|
||||
};
|
||||
|
||||
export const finalReleaseAssets: { [platform: string]: string[] } = {
|
||||
win32: [standaloneZips['win32'], renamedOclifInstallers['win32']],
|
||||
darwin: [standaloneZips['darwin'], renamedOclifInstallers['darwin']],
|
||||
linux: [standaloneZips['linux']],
|
||||
};
|
||||
|
||||
/**
|
||||
* Use the 'pkg' module to create a single large executable file with
|
||||
* the contents of 'node_modules' and the CLI's javascript code.
|
||||
* Also copy a number of native modules (binary '.node' files) that are
|
||||
* compiled during 'npm install' to the 'build-bin' folder, alongside
|
||||
* the single large executable file created by pkg. (This is necessary
|
||||
* because of a pkg limitation that does not allow binary executables
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
const args = [
|
||||
'--target',
|
||||
'host',
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
];
|
||||
console.log('=======================================================');
|
||||
console.log(`execPkg ${args.join(' ')}`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
|
||||
await execPkg(args);
|
||||
|
||||
const paths: Array<[string, string[], string[]]> = [
|
||||
// [platform, [source path], [destination path]]
|
||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||
];
|
||||
await Bluebird.map(paths, ([platform, source, dest]) => {
|
||||
if (platform === '*' || platform === process.platform) {
|
||||
// eg copy from node_modules/open/xdg-open to build-bin/xdg-open
|
||||
return fs.copy(
|
||||
path.join(ROOT, 'node_modules', ...source),
|
||||
path.join(ROOT, 'build-bin', ...dest),
|
||||
);
|
||||
}
|
||||
});
|
||||
const nativeExtensionPaths: string[] = await filehound
|
||||
.create()
|
||||
.paths(path.join(ROOT, 'node_modules'))
|
||||
.ext(['node', 'dll'])
|
||||
.find();
|
||||
|
||||
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
|
||||
|
||||
await Bluebird.map(nativeExtensionPaths, (extPath) =>
|
||||
fs.copy(
|
||||
extPath,
|
||||
extPath.replace(
|
||||
path.join(ROOT, 'node_modules'),
|
||||
path.join(ROOT, 'build-bin'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run some basic tests on the built pkg executable.
|
||||
* TODO: test more than just `balena version -j`; integrate with the
|
||||
* existing mocha/chai CLI command testing.
|
||||
*/
|
||||
async function testPkg() {
|
||||
const pkgBalenaPath = path.join(
|
||||
ROOT,
|
||||
'build-bin',
|
||||
process.platform === 'win32' ? 'balena.exe' : 'balena',
|
||||
);
|
||||
console.log(`Testing standalone package "${pkgBalenaPath}"...`);
|
||||
// Run `balena version -j`, parse its stdout as JSON, and check that the
|
||||
// reported Node.js major version matches semver.major(process.version)
|
||||
const stdout = await getSubprocessStdout(pkgBalenaPath, ['version', '-j']);
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
const balenaVersions: JsonVersions = JSON.parse(stdout);
|
||||
pkgNodeVersion = balenaVersions['Node.js'];
|
||||
pkgNodeMajorVersion = semver.major(pkgNodeVersion);
|
||||
} catch (err) {
|
||||
throw new Error(stripIndent`
|
||||
Error parsing JSON output of "balena version -j": ${err}
|
||||
Original output: "${stdout}"`);
|
||||
}
|
||||
if (semver.major(process.version) !== pkgNodeMajorVersion) {
|
||||
throw new Error(
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the zip file for the standalone 'pkg' bundle previously created
|
||||
* by the buildPkg() function in 'build-bin.ts'.
|
||||
*/
|
||||
async function zipPkg() {
|
||||
const outputFile = standaloneZips[process.platform];
|
||||
if (!outputFile) {
|
||||
throw new Error(
|
||||
`Standalone installer unavailable for platform "${process.platform}"`,
|
||||
);
|
||||
}
|
||||
await fs.mkdirp(path.dirname(outputFile));
|
||||
await new Promise((resolve, reject) => {
|
||||
console.log(`Zipping standalone package to "${outputFile}"...`);
|
||||
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 7 },
|
||||
});
|
||||
archive.directory(path.join(ROOT, 'build-bin'), 'balena-cli');
|
||||
|
||||
const outputStream = fs.createWriteStream(outputFile);
|
||||
|
||||
outputStream.on('close', resolve);
|
||||
outputStream.on('error', reject);
|
||||
|
||||
archive.on('error', reject);
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
await buildPkg();
|
||||
await testPkg();
|
||||
await zipPkg();
|
||||
} catch (error) {
|
||||
console.log(`Error creating or testing standalone zip package:\n ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Standalone zip package build completed`);
|
||||
}
|
||||
|
||||
async function renameInstallerFiles() {
|
||||
if (await fs.pathExists(oclifInstallers[process.platform])) {
|
||||
await fs.rename(
|
||||
oclifInstallers[process.platform],
|
||||
renamedOclifInstallers[process.platform],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
||||
* executable installer by running the balena-io/scripts/shared/sign-exe.sh
|
||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
||||
*/
|
||||
async function signWindowsInstaller() {
|
||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||
const exeName = renamedOclifInstallers[process.platform];
|
||||
const execFileAsync = util.promisify<string, string[], void>(execFile);
|
||||
|
||||
console.log(`Signing installer "${exeName}"`);
|
||||
await execFileAsync(MSYS2_BASH, [
|
||||
'sign-exe.sh',
|
||||
'-f',
|
||||
exeName,
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
*/
|
||||
export async function buildOclifInstaller() {
|
||||
let packOS = '';
|
||||
let packOpts = ['-r', ROOT];
|
||||
if (process.platform === 'darwin') {
|
||||
packOS = 'macos';
|
||||
} else if (process.platform === 'win32') {
|
||||
packOS = 'win';
|
||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||
}
|
||||
if (packOS) {
|
||||
console.log(`Building oclif installer for CLI ${version}`);
|
||||
const packCmd = `pack:${packOS}`;
|
||||
const dirs = [path.join(ROOT, 'dist', packOS)];
|
||||
if (packOS === 'win') {
|
||||
dirs.push(path.join(ROOT, 'tmp', 'win*'));
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
await renameInstallerFiles();
|
||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||
// The macOS installer is automatically signed by oclif (which runs the
|
||||
// `pkgbuild` tool), using the certificate name given in package.json
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the npm `catch-uncommitted` package in order to run it
|
||||
* conditionally, only when:
|
||||
* - A CI env var is set (CI=true), and
|
||||
* - The OS is not Windows. (`catch-uncommitted` fails on Windows)
|
||||
*/
|
||||
export async function catchUncommitted(): Promise<void> {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] CI=${process.env.CI} platform=${process.platform}`);
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
['true', 'yes', '1'].includes(process.env.CI.toLowerCase()) &&
|
||||
process.platform !== 'win32'
|
||||
) {
|
||||
await whichSpawn('npx', [
|
||||
'catch-uncommitted',
|
||||
'--catch-no-git',
|
||||
'--skip-node-versionbot-changes',
|
||||
'--ignore-space-at-eol',
|
||||
]);
|
||||
}
|
||||
}
|
185
automation/capitanodoc/capitanodoc.ts
Normal file
185
automation/capitanodoc/capitanodoc.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/**
|
||||
* @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-oclif/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: [
|
||||
'build/actions-oclif/apps.js',
|
||||
'build/actions-oclif/app/index.js',
|
||||
'build/actions-oclif/app/create.js',
|
||||
'build/actions-oclif/app/rm.js',
|
||||
'build/actions-oclif/app/restart.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [
|
||||
'build/actions-oclif/login.js',
|
||||
'build/actions-oclif/logout.js',
|
||||
'build/actions-oclif/whoami.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [
|
||||
'build/actions/device.js',
|
||||
'build/actions-oclif/device/identify.js',
|
||||
'build/actions-oclif/device/index.js',
|
||||
'build/actions-oclif/device/move.js',
|
||||
'build/actions-oclif/device/reboot.js',
|
||||
'build/actions-oclif/device/register.js',
|
||||
'build/actions-oclif/device/rename.js',
|
||||
'build/actions-oclif/device/rm.js',
|
||||
'build/actions-oclif/device/shutdown.js',
|
||||
'build/actions-oclif/devices/index.js',
|
||||
'build/actions-oclif/devices/supported.js',
|
||||
'build/actions-oclif/device/os-update.js',
|
||||
'build/actions-oclif/device/public-url.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/actions-oclif/envs.js',
|
||||
'build/actions-oclif/env/add.js',
|
||||
'build/actions-oclif/env/rename.js',
|
||||
'build/actions-oclif/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: [
|
||||
'build/actions-oclif/tags.js',
|
||||
'build/actions-oclif/tag/rm.js',
|
||||
'build/actions-oclif/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['build/actions/help.js', 'build/actions-oclif/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [
|
||||
'build/actions-oclif/keys.js',
|
||||
'build/actions-oclif/key/index.js',
|
||||
'build/actions-oclif/key/add.js',
|
||||
'build/actions-oclif/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/actions/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/actions-oclif/scan.js',
|
||||
'build/actions/ssh.js',
|
||||
'build/actions/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/actions-oclif/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: ['build/actions/os.js', 'build/actions-oclif/os/configure.js'],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: ['build/actions/config.js'],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/actions/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/actions/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/actions-oclif/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-oclif/join.js', 'build/actions-oclif/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'),
|
||||
mdParser.getSectionOfTitle('Deprecation policy'),
|
||||
]);
|
||||
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 };
|
92
automation/capitanodoc/index.ts
Normal file
92
automation/capitanodoc/index.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @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') as any);
|
||||
} else {
|
||||
for (const actionName of Object.keys(actions)) {
|
||||
const actionCommand = actions[actionName];
|
||||
commands.push(_.omit(actionCommand, 'action') as any);
|
||||
}
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
// TODO: Currently oclif commands with no `usage` overridden will cause
|
||||
// an error when parsed. This should be improved so that `usage` does not have
|
||||
// to be overridden if not necessary.
|
||||
const command: OclifCommand = require(path.join(process.cwd(), jsFilename))
|
||||
.default as OclifCommand;
|
||||
return [command];
|
||||
}
|
||||
|
||||
/**
|
||||
* Print the CLI docs markdown to stdout.
|
||||
* See package.json for how the output is redirected to a file.
|
||||
*/
|
||||
async function printMarkdown() {
|
||||
try {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
printMarkdown();
|
160
automation/capitanodoc/markdown.ts
Normal file
160
automation/capitanodoc/markdown.ts
Normal file
@ -0,0 +1,160 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { getManualSortCompareFunction } from '../../lib/utils/helpers';
|
||||
import { capitanoizeOclifUsage } from '../../lib/utils/oclif-utils';
|
||||
import { CapitanoCommand, Category, Document, OclifCommand } from './doc-types';
|
||||
import * as utils from './utils';
|
||||
|
||||
function renderCapitanoCommand(command: CapitanoCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.signature)}`, command.help!];
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
result.push('### Options');
|
||||
|
||||
for (const option of command.options!) {
|
||||
if (option == null) {
|
||||
throw new Error(`Undefined option in markdown generation!`);
|
||||
}
|
||||
if (option.description == null) {
|
||||
throw new Error(`Undefined option.description in markdown generation!`);
|
||||
}
|
||||
result.push(
|
||||
`#### ${utils.parseCapitanoOption(option)}`,
|
||||
option.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderOclifCommand(command: OclifCommand): string[] {
|
||||
const result = [`## ${ent.encode(command.usage)}`];
|
||||
const description = (command.description || '')
|
||||
.split('\n')
|
||||
.slice(1) // remove the first line, which oclif uses as help header
|
||||
.join('\n')
|
||||
.trim();
|
||||
result.push(description);
|
||||
|
||||
if (!_.isEmpty(command.examples)) {
|
||||
result.push('Examples:', command.examples!.map((v) => `\t${v}`).join('\n'));
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.flags)) {
|
||||
result.push('### Options');
|
||||
for (const [name, flag] of Object.entries(command.flags!)) {
|
||||
if (name === 'help') {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
result.push(`#### ${flagUsage}`);
|
||||
result.push(flag.description || '');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function renderCategory(category: Category): string[] {
|
||||
const result = [`# ${category.title}`];
|
||||
for (const command of category.commands) {
|
||||
result.push(
|
||||
...(typeof command === 'object'
|
||||
? renderCapitanoCommand(command)
|
||||
: renderOclifCommand(command)),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAnchor(cmdSignature: string): string {
|
||||
return `#${_.trim(cmdSignature.replace(/\W+/g, '-'), '-').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function renderToc(categories: Category[]): string[] {
|
||||
const result = [`# CLI Command Reference`];
|
||||
|
||||
for (const category of categories) {
|
||||
result.push(`- ${category.title}`);
|
||||
result.push(
|
||||
category.commands
|
||||
.map((command) => {
|
||||
const signature =
|
||||
typeof command === 'object'
|
||||
? command.signature // Capitano
|
||||
: capitanoizeOclifUsage(command.usage); // oclif
|
||||
return `\t- [${ent.encode(signature)}](${getAnchor(signature)})`;
|
||||
})
|
||||
.join('\n'),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const manualCategorySorting: { [category: string]: string[] } = {
|
||||
'Environment Variables': ['envs', 'env rm', 'env add', 'env rename'],
|
||||
OS: [
|
||||
'os versions',
|
||||
'os download',
|
||||
'os build config',
|
||||
'os configure',
|
||||
'os initialize',
|
||||
],
|
||||
};
|
||||
|
||||
function sortCommands(doc: Document): void {
|
||||
for (const category of doc.categories) {
|
||||
if (category.title in manualCategorySorting) {
|
||||
category.commands = category.commands.sort(
|
||||
getManualSortCompareFunction<CapitanoCommand | OclifCommand, string>(
|
||||
manualCategorySorting[category.title],
|
||||
(cmd: CapitanoCommand | OclifCommand, x: string) =>
|
||||
typeof cmd === 'object' // Capitano vs oclif command
|
||||
? cmd.signature.replace(/\W+/g, ' ').includes(x)
|
||||
: (cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function render(doc: Document) {
|
||||
sortCommands(doc);
|
||||
const result = [
|
||||
`# ${doc.title}`,
|
||||
doc.introduction,
|
||||
...renderToc(doc.categories),
|
||||
];
|
||||
for (const category of doc.categories) {
|
||||
result.push(...renderCategory(category));
|
||||
}
|
||||
return result.join('\n\n');
|
||||
}
|
136
automation/capitanodoc/utils.ts
Normal file
136
automation/capitanodoc/utils.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @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 type { OptionDefinition } from 'capitano';
|
||||
import * as ent from 'ent';
|
||||
import * as fs from 'fs';
|
||||
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 (Array.isArray(option.alias)) {
|
||||
for (const alias of option.alias) {
|
||||
result += `, ${getOptionSignature(alias)}`;
|
||||
}
|
||||
} else if (typeof option.alias === 'string') {
|
||||
result += `, ${getOptionSignature(option.alias)}`;
|
||||
}
|
||||
|
||||
if (option.parameter) {
|
||||
result += ` <${option.parameter}>`;
|
||||
}
|
||||
|
||||
return ent.encode(result);
|
||||
}
|
||||
|
||||
export class MarkdownFileParser {
|
||||
constructor(public mdFilePath: string) {}
|
||||
|
||||
/**
|
||||
* Extract the lines of a markdown document section with the given title.
|
||||
* For example, consider this sample markdown document:
|
||||
* ```
|
||||
* # balena CLI
|
||||
*
|
||||
* ## Introduction
|
||||
* Lorem ipsum dolor sit amet, consectetur adipiscing elit,
|
||||
*
|
||||
* ## Getting Started
|
||||
* sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
*
|
||||
* ### Prerequisites
|
||||
* - Foo
|
||||
* - Bar
|
||||
*
|
||||
* ## Support
|
||||
* Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
|
||||
* ```
|
||||
*
|
||||
* Calling getSectionOfTitle('Getting Started') for the markdown doc above
|
||||
* returns everything from line '## Getting Started' (included) to line
|
||||
* '## Support' (excluded). This method counts the number of '#' characters
|
||||
* to determine that subsections should be included as part of the parent
|
||||
* section.
|
||||
*
|
||||
* @param title The section title without '#' chars, eg. 'Getting Started'
|
||||
*/
|
||||
public async getSectionOfTitle(
|
||||
title: string,
|
||||
includeSubsections = true,
|
||||
): Promise<string> {
|
||||
let foundSectionLines: string[];
|
||||
let foundSectionLevel = 0;
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: fs.createReadStream(this.mdFilePath),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
// try to match a line like "## Getting Started", where the number
|
||||
// of '#' characters is the sectionLevel ('##' -> 2), and the
|
||||
// sectionTitle is "Getting Started"
|
||||
const match = /^(#+)\s+(.+)/.exec(line);
|
||||
if (match) {
|
||||
const sectionLevel = match[1].length;
|
||||
const sectionTitle = match[2];
|
||||
|
||||
// If the target section had already been found: append a line, or end it
|
||||
if (foundSectionLines) {
|
||||
if (!includeSubsections || sectionLevel <= foundSectionLevel) {
|
||||
// end previously found section
|
||||
rl.close();
|
||||
}
|
||||
} else if (sectionTitle === title) {
|
||||
// found the target section
|
||||
foundSectionLevel = sectionLevel;
|
||||
foundSectionLines = [];
|
||||
}
|
||||
}
|
||||
if (foundSectionLines) {
|
||||
foundSectionLines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
rl.on('close', () => {
|
||||
if (foundSectionLines) {
|
||||
resolve(foundSectionLines.join('\n'));
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Markdown section not found: title="${title}" file="${this.mdFilePath}"`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
84
automation/check-doc.js
Normal file
84
automation/check-doc.js
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { stripIndent } = require('common-tags');
|
||||
const _ = require('lodash');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
|
||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||
|
||||
/**
|
||||
* Compare the timestamp of cli.markdown with the timestamp of staged files,
|
||||
* issuing an error if cli.markdown is older.
|
||||
* If cli.markdown does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
]);
|
||||
const stagedFiles = _.uniq([
|
||||
...gitStatus.created,
|
||||
...gitStatus.staged,
|
||||
...gitStatus.renamed.map((o) => o.to),
|
||||
])
|
||||
// select only staged files that start with lib/ or typings/
|
||||
.filter((f) => f.match(/^(lib|typings)[/\\]/))
|
||||
.map((f) => path.join(ROOT, f));
|
||||
|
||||
const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
|
||||
fStats.forEach((fStat, index) => {
|
||||
if (fStat.mtimeMs > docStat.mtimeMs) {
|
||||
const fPath = stagedFiles[index];
|
||||
throw new Error(stripIndent`
|
||||
--------------------------------------------------------------------------------
|
||||
ERROR: at least one staged file: "${fPath}"
|
||||
has a more recent modification timestamp than the documentation file:
|
||||
"${docFile}"
|
||||
|
||||
This probably means that \`npm run build\` or \`npm test\` have not been executed,
|
||||
and this error can be fixed by doing so. Running \`npm run build\` or \`npm test\`
|
||||
before commiting is required in order to update the CLI markdown documentation
|
||||
(in case any command-line options were updated, added or removed) and also to
|
||||
catch Typescript type check errors sooner and reduce overall waiting time, given
|
||||
that the CI build/tests are currently rather lengthy.
|
||||
|
||||
If you need/wish to bypass this check without running \`npm run build\`, run:
|
||||
npx touch -am "${docFile}"
|
||||
and then try again.
|
||||
--------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await checkBuildTimestamps();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
57
automation/check-npm-version.js
Normal file
57
automation/check-npm-version.js
Normal file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Check that semver v1 is greater than or equal to semver v2.
|
||||
*
|
||||
* We don't `require('semver')` to allow this script to be run as a npm
|
||||
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
if (match == null) {
|
||||
throw new Error(`Invalid semver version: ${version}`);
|
||||
}
|
||||
const [, major, minor, patch] = match;
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
} else if (v1Array[i] > v2Array[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function checkNpmVersion() {
|
||||
const execSync = require('child_process').execSync;
|
||||
const npmVersion = execSync('npm --version').toString().trim();
|
||||
const requiredVersion = '6.9.0';
|
||||
if (!semverGte(npmVersion, requiredVersion)) {
|
||||
// In case you take issue with the error message below:
|
||||
// "At this point, however, your 'npm-shrinkwrap.json' file has
|
||||
// already been damaged"
|
||||
// ... and think: "why not add the check to the 'preinstall' hook?",
|
||||
// the reason is that it would unnecessarily prevent end users from
|
||||
// using npm v6.4.1 that ships with Node 8. (It is OK for the
|
||||
// shrinkwrap file to get damaged if it is not going to be reused.)
|
||||
console.error(`\
|
||||
-------------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
248
automation/deploy-bin.ts
Normal file
248
automation/deploy-bin.ts
Normal file
@ -0,0 +1,248 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { finalReleaseAssets, version } from './build-bin';
|
||||
|
||||
const { GITHUB_TOKEN } = process.env;
|
||||
|
||||
/**
|
||||
* Create or update a release in GitHub's releases page, uploading the
|
||||
* installer files (standalone zip + native oclif installers).
|
||||
*/
|
||||
export async function createGitHubRelease() {
|
||||
console.log(`Publishing release ${version} to GitHub`);
|
||||
const publishRelease = await import('publish-release');
|
||||
const ghRelease = await Bluebird.fromCallback(
|
||||
publishRelease.bind(null, {
|
||||
token: GITHUB_TOKEN || '',
|
||||
owner: 'balena-io',
|
||||
repo: 'balena-cli',
|
||||
tag: version,
|
||||
name: `balena-CLI ${version}`,
|
||||
reuseRelease: true,
|
||||
assets: finalReleaseAssets[process.platform],
|
||||
}),
|
||||
);
|
||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-level function to create a CLI release in GitHub's releases page:
|
||||
* call zipStandaloneInstaller(), rename the files as we'd like them to
|
||||
* display on the releases page, and call createGitHubRelease() to upload
|
||||
* the files.
|
||||
*/
|
||||
export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
|
||||
require('@octokit/plugin-throttling'),
|
||||
);
|
||||
return new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
throttle: {
|
||||
onRateLimit: (retryAfter: number, options: any) => {
|
||||
console.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
// retries 3 times
|
||||
if (options.request.retryCount < 3) {
|
||||
console.log(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
onAbuseLimit: (_retryAfter: number, options: any) => {
|
||||
// does not retry, only logs a warning
|
||||
console.warn(
|
||||
`Abuse detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Extract pagination information (current page, total pages, ordinal number)
|
||||
* from the 'link' response header (example below), using the parse-link-header
|
||||
* npm package:
|
||||
* "link": "<https://api.github.com/repositories/187370853/releases?per_page=2&page=2>; rel=\"next\",
|
||||
* <https://api.github.com/repositories/187370853/releases?per_page=2&page=3>; rel=\"last\""
|
||||
*
|
||||
* @param response Octokit response object (including response.headers.link)
|
||||
* @param perPageDefault Default per_page pagination value if missing in URL
|
||||
* @return Object where 'page' is the current page number (1-based),
|
||||
* 'pages' is the total number of pages, and 'ordinal' is the ordinal number
|
||||
* (3rd, 4th, 5th...) of the first item in the current page.
|
||||
*/
|
||||
function getPageNumbers(
|
||||
response: any,
|
||||
perPageDefault: number,
|
||||
): { page: number; pages: number; ordinal: number } {
|
||||
const res = { page: 1, pages: 1, ordinal: 1 };
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = require('parse-link-header');
|
||||
const parsed = parse(response.headers.link);
|
||||
let perPage = perPageDefault;
|
||||
if (parsed.next) {
|
||||
if (parsed.next.per_page) {
|
||||
perPage = parseInt(parsed.next.per_page, 10);
|
||||
}
|
||||
res.page = parseInt(parsed.next.page, 10) - 1;
|
||||
res.pages = parseInt(parsed.last.page, 10);
|
||||
} else {
|
||||
if (parsed.prev.per_page) {
|
||||
perPage = parseInt(parsed.prev.per_page, 10);
|
||||
}
|
||||
res.page = res.pages = parseInt(parsed.prev.page, 10) + 1;
|
||||
}
|
||||
res.ordinal = (res.page - 1) * perPage + 1;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over every GitHub release in the given owner/repo, check whether
|
||||
* its tag_name matches against the affectedVersions semver spec, and if so
|
||||
* replace its release description (body) with the given newDescription value.
|
||||
* @param owner GitHub repo owner, e.g. 'balena-io' or 'pdcastro'
|
||||
* @param repo GitHub repo, e.g. 'balena-cli'
|
||||
* @param affectedVersions Semver spec, e.g. '2.6.1 - 7.10.9 || 8.0.0'
|
||||
* @param newDescription New release description (body)
|
||||
* @param editID Short string present in newDescription, e.g. '[AA101]', that
|
||||
* can be searched to determine whether that release has already been updated.
|
||||
*/
|
||||
async function updateGitHubReleaseDescriptions(
|
||||
owner: string,
|
||||
repo: string,
|
||||
affectedVersions: string,
|
||||
newDescription: string,
|
||||
editID: string,
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||
owner,
|
||||
repo,
|
||||
per_page: perPage,
|
||||
});
|
||||
let errCount = 0;
|
||||
for await (const response of octokit.paginate.iterator(options)) {
|
||||
const { page: thisPage, pages: totalPages, ordinal } = getPageNumbers(
|
||||
response,
|
||||
perPage,
|
||||
);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
if (!cliRelease.id) {
|
||||
console.error(
|
||||
`${prefix} Error: missing release ID (errCount=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const skipMsg = `${prefix} skipping release "${cliRelease.tag_name}" (${cliRelease.id})`;
|
||||
if (cliRelease.draft === true) {
|
||||
console.info(`${skipMsg}: draft release`);
|
||||
continue;
|
||||
} else if (cliRelease.body && cliRelease.body.includes(editID)) {
|
||||
console.info(`${skipMsg}: already updated`);
|
||||
continue;
|
||||
} else if (!semver.satisfies(cliRelease.tag_name, affectedVersions)) {
|
||||
console.info(`${skipMsg}: outside version range`);
|
||||
continue;
|
||||
} else {
|
||||
const updatedRelease = {
|
||||
owner,
|
||||
repo,
|
||||
release_id: cliRelease.id,
|
||||
body: newDescription,
|
||||
};
|
||||
let oldBodyPreview = cliRelease.body;
|
||||
if (oldBodyPreview) {
|
||||
oldBodyPreview = oldBodyPreview.replace(/\s+/g, ' ').trim();
|
||||
if (oldBodyPreview.length > 12) {
|
||||
oldBodyPreview = oldBodyPreview.substring(0, 9) + '...';
|
||||
}
|
||||
}
|
||||
console.info(
|
||||
`${prefix} updating release "${cliRelease.tag_name}" (${cliRelease.id}) old body="${oldBodyPreview}"`,
|
||||
);
|
||||
try {
|
||||
await octokit.repos.updateRelease(updatedRelease);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`${skipMsg}: Error: ${err.message} (count=${++errCount})`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a warning description to CLI releases affected by a mixpanel tracking
|
||||
* security issue (#1359). This function can be executed "manually" with the
|
||||
* following command line:
|
||||
*
|
||||
* npx ts-node --type-check -P automation/tsconfig.json automation/run.ts fix1359
|
||||
*/
|
||||
export async function updateDescriptionOfReleasesAffectedByIssue1359() {
|
||||
// Run only on Linux/Node10, instead of all platform/Node combinations.
|
||||
// (It could have been any other platform, as long as it only runs once.)
|
||||
if (process.platform !== 'linux' || semver.major(process.version) !== 10) {
|
||||
return;
|
||||
}
|
||||
const owner = 'balena-io';
|
||||
const repo = 'balena-cli';
|
||||
const affectedVersions =
|
||||
'2.6.1 - 7.10.9 || 8.0.0 - 8.1.0 || 9.0.0 - 9.15.6 || 10.0.0 - 10.17.5 || 11.0.0 - 11.7.2';
|
||||
const editID = '[AA100]';
|
||||
let newDescription = `
|
||||
Please note: the "login" command in this release is affected by a
|
||||
security issue fixed in versions
|
||||
[7.10.10](https://github.com/balena-io/balena-cli/releases/tag/v7.10.10),
|
||||
[8.1.1](https://github.com/balena-io/balena-cli/releases/tag/v8.1.1),
|
||||
[9.15.7](https://github.com/balena-io/balena-cli/releases/tag/v9.15.7),
|
||||
[10.17.6](https://github.com/balena-io/balena-cli/releases/tag/v10.17.6),
|
||||
[11.7.3](https://github.com/balena-io/balena-cli/releases/tag/v11.7.3)
|
||||
and later. If you need to use this version, avoid passing your password,
|
||||
keys or tokens as command-line arguments. ${editID}`;
|
||||
// remove line breaks and collapse white space
|
||||
newDescription = newDescription.replace(/\s+/g, ' ').trim();
|
||||
await updateGitHubReleaseDescriptions(
|
||||
owner,
|
||||
repo,
|
||||
affectedVersions,
|
||||
newDescription,
|
||||
editID,
|
||||
);
|
||||
}
|
121
automation/run.ts
Normal file
121
automation/run.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import {
|
||||
buildOclifInstaller,
|
||||
buildStandaloneZip,
|
||||
catchUncommitted,
|
||||
} from './build-bin';
|
||||
import {
|
||||
release,
|
||||
updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
} from './deploy-bin';
|
||||
import { fixPathForMsys, ROOT, runUnderMsys } from './utils';
|
||||
|
||||
// DEBUG set to falsy for negative values else is truthy
|
||||
process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
process.env.DEBUG?.toLowerCase(),
|
||||
)
|
||||
? ''
|
||||
: '1';
|
||||
|
||||
function exitWithError(error: Error | string): never {
|
||||
console.error(`Error: ${error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trivial command-line parser. Check whether the command-line argument is one
|
||||
* of the following strings, then call the appropriate functions:
|
||||
* 'build:installer' (to build a native oclif installer)
|
||||
* 'build:standalone' (to build a standalone pkg package)
|
||||
* 'release' (to create/update a GitHub release)
|
||||
*
|
||||
* In the case of 'build:installer', also call runUnderMsys() to switch the
|
||||
* shell from cmd.exe to MSYS2 bash.exe.
|
||||
*
|
||||
* @param args Arguments to parse (default is process.argv.slice(2))
|
||||
*/
|
||||
export async function run(args?: string[]) {
|
||||
args = args || process.argv.slice(2);
|
||||
console.log(`automation/run.ts process.argv=[${process.argv}]\n`);
|
||||
console.log(`automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
'build:standalone': buildStandaloneZip,
|
||||
'catch-uncommitted': catchUncommitted,
|
||||
fix1359: updateDescriptionOfReleasesAffectedByIssue1359,
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If runUnderMsys() is called to re-execute this script under MSYS2,
|
||||
// the current working dir becomes the MSYS2 homedir, so we change back.
|
||||
process.chdir(ROOT);
|
||||
|
||||
// The BUILD_TMP env var is used as an alternative location for oclif
|
||||
// (patched) to copy/extract the CLI files, run npm install and then
|
||||
// create the NSIS executable installer for Windows. This was necessary
|
||||
// to avoid issues with a 260-char limit on Windows paths (possibly a
|
||||
// limitation of some library used by NSIS), as the "current working dir"
|
||||
// provided by balena CI is a rather long path to start with.
|
||||
if (process.platform === 'win32' && !process.env.BUILD_TMP) {
|
||||
const randID = require('crypto')
|
||||
.randomBytes(6)
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_'); // base64url (RFC 4648)
|
||||
process.env.BUILD_TMP = `C:\\tmp\\${randID}`;
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
try {
|
||||
if (arg === 'build:installer' && process.platform === 'win32') {
|
||||
// ensure running under MSYS2
|
||||
if (!process.env.MSYSTEM) {
|
||||
process.env.MSYS2_PATH_TYPE = 'inherit';
|
||||
await runUnderMsys([
|
||||
fixPathForMsys(process.argv[0]),
|
||||
fixPathForMsys(process.argv[1]),
|
||||
arg,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
if (process.env.MSYS2_PATH_TYPE !== 'inherit') {
|
||||
throw new Error(
|
||||
'the MSYS2_PATH_TYPE env var must be set to "inherit"',
|
||||
);
|
||||
}
|
||||
}
|
||||
const cmdFunc = commands[arg];
|
||||
await cmdFunc();
|
||||
} catch (err) {
|
||||
return exitWithError(`"${arg}": ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
140
automation/update-module.ts
Normal file
140
automation/update-module.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { exec } from 'child_process';
|
||||
import * as semver from 'semver';
|
||||
|
||||
const changeTypes = ['major', 'minor', 'patch'] as const;
|
||||
|
||||
const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
maybeChangeType = maybeChangeType.toLowerCase();
|
||||
switch (maybeChangeType) {
|
||||
case 'patch':
|
||||
case 'minor':
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const compareSemverChangeType = (oldVersion: string, newVersion: string) => {
|
||||
const oldSemver = semver.parse(oldVersion)!;
|
||||
const newSemver = semver.parse(newVersion)!;
|
||||
|
||||
for (const changeType of changeTypes) {
|
||||
if (oldSemver[changeType] !== newSemver[changeType]) {
|
||||
return changeType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const run = async (cmd: string) => {
|
||||
console.info(`Running '${cmd}'`);
|
||||
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
||||
const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout.pipe(process.stdout);
|
||||
p.stderr.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
const getVersion = async (module: string): Promise<string> => {
|
||||
const { stdout } = await run(`npm ls --json --depth 0 ${module}`);
|
||||
return JSON.parse(stdout).dependencies[module].version;
|
||||
};
|
||||
|
||||
interface Upstream {
|
||||
repo: string;
|
||||
url: string;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
const getUpstreams = async () => {
|
||||
const fs = await import('fs');
|
||||
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
|
||||
|
||||
const yaml = await import('js-yaml');
|
||||
const { upstream } = yaml.safeLoad(repoYaml) as {
|
||||
upstream: Upstream[];
|
||||
};
|
||||
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
Usage: npm run update ${upstreamName} $version [$changeType=minor]
|
||||
|
||||
Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
|
||||
`,
|
||||
);
|
||||
return process.exit(1);
|
||||
};
|
||||
|
||||
// TODO: Drop the wrapper function once we move to TS 3.8,
|
||||
// which will support top level await.
|
||||
async function main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
|
||||
const oldVersion = await getVersion(packageName);
|
||||
await run(`npm install ${packageName}@${process.argv[3]}`);
|
||||
const newVersion = await getVersion(packageName);
|
||||
if (newVersion === oldVersion) {
|
||||
console.error(`Already on version '${newVersion}'`);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
const semverChangeType = compareSemverChangeType(oldVersion, newVersion);
|
||||
|
||||
const changeType = process.argv[4]
|
||||
? // if the caller specified a change type, use that one
|
||||
validateChangeType(process.argv[4])
|
||||
: // use the same change type as in the dependency, but avoid major bumps
|
||||
semverChangeType && semverChangeType !== 'major'
|
||||
? semverChangeType
|
||||
: 'minor';
|
||||
console.log(`Using Change-type: ${changeType}`);
|
||||
|
||||
let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
|
||||
currentBranch = currentBranch.trim();
|
||||
console.log(`Currenty on branch: '${currentBranch}'`);
|
||||
if (currentBranch === 'master') {
|
||||
await run(`git checkout -b "update-${upstreamName}-${newVersion}"`);
|
||||
}
|
||||
|
||||
await run(`git add package.json npm-shrinkwrap.json`);
|
||||
await run(
|
||||
`git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`,
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
174
automation/utils.ts
Normal file
174
automation/utils.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
|
||||
export const MSYS2_BASH = 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
export function loadPackageJson() {
|
||||
return require(path.join(ROOT, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert e.g. 'C:\myfolder' -> '/C/myfolder' so that the path can be given
|
||||
* as argument to "unix tools" like 'tar' under MSYS or MSYS2 on Windows.
|
||||
*/
|
||||
export function fixPathForMsys(p: string): string {
|
||||
return p.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the MSYS2 bash.exe shell in a child process (child_process.spawn()).
|
||||
* The given argv arguments are escaped using the 'shell-escape' package,
|
||||
* so that backslashes in Windows paths, and other bash-special characters,
|
||||
* are preserved. If argv is not provided, defaults to process.argv, to the
|
||||
* effect that this current (parent) process is re-executed under MSYS2 bash.
|
||||
* This is useful to change the default shell from cmd.exe to MSYS2 bash on
|
||||
* Windows.
|
||||
* @param argv Arguments to be shell-escaped and given to MSYS2 bash.exe.
|
||||
*/
|
||||
export async function runUnderMsys(argv?: string[]) {
|
||||
const newArgv = argv || process.argv;
|
||||
await new Promise((resolve, reject) => {
|
||||
const args = ['-lc', shellEscape(newArgv)];
|
||||
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
|
||||
child.on('close', (code) => {
|
||||
if (code) {
|
||||
console.log(`runUnderMsys: child process exited with code ${code}`);
|
||||
reject(code);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the executable at execPath as a child process, and resolve a promise
|
||||
* to the executable's stdout output as a string. Reject the promise if
|
||||
* anything is printed to stderr, or if the child process exits with a
|
||||
* non-zero exit code.
|
||||
* @param execPath Executable path
|
||||
* @param args Command-line argument for the executable
|
||||
*/
|
||||
export async function getSubprocessStdout(
|
||||
execPath: string,
|
||||
args: string[],
|
||||
): Promise<string> {
|
||||
const child = spawn(execPath, args);
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('error', reject);
|
||||
child.stderr.on('error', reject);
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
try {
|
||||
stdout = data.toString();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.stderr.on('data', (data: Buffer) => {
|
||||
try {
|
||||
const stderr = data.toString();
|
||||
|
||||
// ignore any debug lines, but ensure that we parse
|
||||
// every line provided to the stderr stream
|
||||
const lines = _.filter(
|
||||
stderr.trim().split(/\r?\n/),
|
||||
(line) => !line.startsWith('[debug]'),
|
||||
);
|
||||
if (lines.length > 0) {
|
||||
reject(
|
||||
new Error(`"${execPath}": non-empty stderr "${lines.join('\n')}"`),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
child.on('exit', (code: number) => {
|
||||
if (code) {
|
||||
reject(new Error(`"${execPath}": non-zero exit code "${code}"`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling wrapper around the npm `which` package:
|
||||
* "Like the unix which utility. Finds the first instance of a specified
|
||||
* executable in the PATH environment variable. Does not cache the results,
|
||||
* so hash -r is not needed when the PATH changes."
|
||||
*
|
||||
* @param program Basename of a program, for example 'ssh'
|
||||
* @returns The program's full path, e.g. 'C:\WINDOWS\System32\OpenSSH\ssh.EXE'
|
||||
*/
|
||||
export async function which(program: string): Promise<string> {
|
||||
const whichMod = await import('which');
|
||||
let programPath: string;
|
||||
try {
|
||||
programPath = await whichMod(program);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`'${program}' program not found. Is it installed?`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return programPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call which(programName) and spawn() with the given arguments. Throw an error
|
||||
* if the process exit code is not zero.
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args: string[],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
let exitCode: number | undefined;
|
||||
try {
|
||||
exitCode = await new Promise<number>((resolve, reject) => {
|
||||
try {
|
||||
spawn(program, args, { stdio: 'inherit' })
|
||||
.on('error', reject)
|
||||
.on('close', resolve);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
if (error || exitCode) {
|
||||
const msg = [
|
||||
`${programName} failed with exit code ${exitCode}:`,
|
||||
`"${program}" [${args}]`,
|
||||
];
|
||||
if (error) {
|
||||
msg.push(`${error}`);
|
||||
}
|
||||
throw new Error(msg.join('\n'));
|
||||
}
|
||||
}
|
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
|
16
bin/balena
Executable file
16
bin/balena
Executable file
@ -0,0 +1,16 @@
|
||||
#!/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';
|
||||
|
||||
// Disable oclif registering ts-node
|
||||
process.env.OCLIF_TS_NODE = 0;
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
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({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: '.fast-boot.json',
|
||||
});
|
||||
|
||||
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' ]
|
||||
},
|
||||
]
|
127
coffeelint.json
127
coffeelint.json
@ -1,127 +0,0 @@
|
||||
{
|
||||
"coffeescript_error": {
|
||||
"level": "error"
|
||||
},
|
||||
"arrow_spacing": {
|
||||
"name": "arrow_spacing",
|
||||
"level": "error"
|
||||
},
|
||||
"no_tabs": {
|
||||
"name": "no_tabs",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_trailing_whitespace": {
|
||||
"name": "no_trailing_whitespace",
|
||||
"level": "error",
|
||||
"allowed_in_comments": false,
|
||||
"allowed_in_empty_lines": false
|
||||
},
|
||||
"max_line_length": {
|
||||
"name": "max_line_length",
|
||||
"value": 120,
|
||||
"level": "error",
|
||||
"limitComments": true
|
||||
},
|
||||
"line_endings": {
|
||||
"name": "line_endings",
|
||||
"level": "ignore",
|
||||
"value": "unix"
|
||||
},
|
||||
"no_trailing_semicolons": {
|
||||
"name": "no_trailing_semicolons",
|
||||
"level": "error"
|
||||
},
|
||||
"indentation": {
|
||||
"name": "indentation",
|
||||
"value": 1,
|
||||
"level": "error"
|
||||
},
|
||||
"camel_case_classes": {
|
||||
"name": "camel_case_classes",
|
||||
"level": "error"
|
||||
},
|
||||
"colon_assignment_spacing": {
|
||||
"name": "colon_assignment_spacing",
|
||||
"level": "error",
|
||||
"spacing": {
|
||||
"left": 0,
|
||||
"right": 1
|
||||
}
|
||||
},
|
||||
"no_implicit_braces": {
|
||||
"name": "no_implicit_braces",
|
||||
"level": "ignore",
|
||||
"strict": false
|
||||
},
|
||||
"no_plusplus": {
|
||||
"name": "no_plusplus",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_throwing_strings": {
|
||||
"name": "no_throwing_strings",
|
||||
"level": "error"
|
||||
},
|
||||
"no_backticks": {
|
||||
"name": "no_backticks",
|
||||
"level": "error"
|
||||
},
|
||||
"no_implicit_parens": {
|
||||
"name": "no_implicit_parens",
|
||||
"strict": false,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_empty_param_list": {
|
||||
"name": "no_empty_param_list",
|
||||
"level": "error"
|
||||
},
|
||||
"no_stand_alone_at": {
|
||||
"name": "no_stand_alone_at",
|
||||
"level": "ignore"
|
||||
},
|
||||
"space_operators": {
|
||||
"name": "space_operators",
|
||||
"level": "error"
|
||||
},
|
||||
"duplicate_key": {
|
||||
"name": "duplicate_key",
|
||||
"level": "error"
|
||||
},
|
||||
"empty_constructor_needs_parens": {
|
||||
"name": "empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"cyclomatic_complexity": {
|
||||
"name": "cyclomatic_complexity",
|
||||
"value": 10,
|
||||
"level": "ignore"
|
||||
},
|
||||
"newlines_after_classes": {
|
||||
"name": "newlines_after_classes",
|
||||
"value": 3,
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_fat_arrows": {
|
||||
"name": "no_unnecessary_fat_arrows",
|
||||
"level": "error"
|
||||
},
|
||||
"missing_fat_arrows": {
|
||||
"name": "missing_fat_arrows",
|
||||
"level": "ignore"
|
||||
},
|
||||
"non_empty_constructor_needs_parens": {
|
||||
"name": "non_empty_constructor_needs_parens",
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_unnecessary_double_quotes": {
|
||||
"name": "no_unnecessary_double_quotes",
|
||||
"level": "error"
|
||||
},
|
||||
"no_debugger": {
|
||||
"name": "no_debugger",
|
||||
"level": "warn"
|
||||
},
|
||||
"no_interpolation_in_single_quotes": {
|
||||
"name": "no_interpolation_in_single_quotes",
|
||||
"level": "error"
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
2677
doc/cli.markdown
2677
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,50 +0,0 @@
|
||||
path = require('path')
|
||||
gulp = require('gulp')
|
||||
coffee = require('gulp-coffee')
|
||||
coffeelint = require('gulp-coffeelint')
|
||||
inlinesource = require('gulp-inline-source')
|
||||
mocha = require('gulp-mocha')
|
||||
shell = require('gulp-shell')
|
||||
packageJSON = require('./package.json')
|
||||
|
||||
OPTIONS =
|
||||
config:
|
||||
coffeelint: path.join(__dirname, 'coffeelint.json')
|
||||
files:
|
||||
coffee: [ 'lib/**/*.coffee', 'gulpfile.coffee' ]
|
||||
app: 'lib/**/*.coffee'
|
||||
tests: 'tests/**/*.spec.coffee'
|
||||
pages: 'lib/auth/pages/*.ejs'
|
||||
directories:
|
||||
build: 'build/'
|
||||
|
||||
gulp.task 'pages', ->
|
||||
gulp.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages'))
|
||||
|
||||
gulp.task 'coffee', [ 'lint' ], ->
|
||||
gulp.src(OPTIONS.files.app)
|
||||
.pipe(coffee(bare: true, header: true))
|
||||
.pipe(gulp.dest(OPTIONS.directories.build))
|
||||
|
||||
gulp.task 'lint', ->
|
||||
gulp.src(OPTIONS.files.coffee)
|
||||
.pipe(coffeelint({
|
||||
optFile: OPTIONS.config.coffeelint
|
||||
}))
|
||||
.pipe(coffeelint.reporter())
|
||||
|
||||
gulp.task 'test', ->
|
||||
gulp.src(OPTIONS.files.tests, read: false)
|
||||
.pipe(mocha({
|
||||
reporter: 'min'
|
||||
}))
|
||||
|
||||
gulp.task 'build', [
|
||||
'coffee',
|
||||
'pages'
|
||||
]
|
||||
|
||||
gulp.task 'watch', [ 'build' ], ->
|
||||
gulp.watch([ OPTIONS.files.coffee ], [ 'build' ])
|
15
gulpfile.js
Normal file
15
gulpfile.js
Normal file
@ -0,0 +1,15 @@
|
||||
const gulp = require('gulp');
|
||||
const inlinesource = require('gulp-inline-source');
|
||||
|
||||
const OPTIONS = {
|
||||
files: {
|
||||
pages: 'lib/auth/pages/*.ejs',
|
||||
},
|
||||
};
|
||||
|
||||
gulp.task('pages', () =>
|
||||
gulp
|
||||
.src(OPTIONS.files.pages)
|
||||
.pipe(inlinesource())
|
||||
.pipe(gulp.dest('build/auth/pages')),
|
||||
);
|
86
lib/actions-oclif/api-key/generate.ts
Normal file
86
lib/actions-oclif/api-key/generate.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
|
||||
Generate a new balenaCloud API key for the current user, with the given
|
||||
name. The key will be logged to the console.
|
||||
|
||||
This key can be used to log into the CLI using 'balena login --token <key>',
|
||||
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
|
||||
`;
|
||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'api-key generate <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||
|
||||
let key;
|
||||
try {
|
||||
key = await getBalenaSdk().models.apiKey.create(params.name);
|
||||
} catch (e) {
|
||||
if (e.name === 'BalenaNotLoggedIn') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
This command cannot be run when logged in with an API key.
|
||||
Please login again with 'balena login' and select an alternative method.
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(stripIndent`
|
||||
Registered api key '${params.name}':
|
||||
|
||||
${key}
|
||||
|
||||
This key will not be shown again, so please save it now.
|
||||
`);
|
||||
}
|
||||
}
|
102
lib/actions-oclif/app/create.ts
Normal file
102
lib/actions-oclif/app/create.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an application.
|
||||
|
||||
Create a new balena application.
|
||||
|
||||
You can specify the application device type with the \`--type\` option.
|
||||
Otherwise, an interactive dropdown will be shown for you to select from.
|
||||
|
||||
You can see a list of supported device types with:
|
||||
|
||||
$ balena devices supported
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'application device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Create application
|
||||
const deviceType = options.type || (await patterns.selectDeviceType());
|
||||
let application: import('balena-sdk').Application;
|
||||
try {
|
||||
application = await balena.models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
});
|
||||
} catch (err) {
|
||||
// BalenaRequestError: Request error: Unique key constraint violated
|
||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(
|
||||
`Error: application "${params.name}" already exists`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
console.info(
|
||||
`Application created: ${application.slug} (${application.device_type}, id ${application.id})`,
|
||||
);
|
||||
}
|
||||
}
|
75
lib/actions-oclif/app/index.ts
Normal file
75
lib/actions-oclif/app/index.ts
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single application.
|
||||
|
||||
Display detailed information about a single balena application.
|
||||
`;
|
||||
public static examples = ['$ balena app MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
||||
|
||||
const application = await getBalenaSdk().models.application.get(
|
||||
tryAsInteger(params.name),
|
||||
);
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
`$${application.app_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
62
lib/actions-oclif/app/restart.ts
Normal file
62
lib/actions-oclif/app/restart.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart an application.
|
||||
|
||||
Restart all devices that belongs to a certain application.
|
||||
`;
|
||||
public static examples = ['$ balena app restart MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app restart <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
|
||||
await getBalenaSdk().models.application.restart(tryAsInteger(params.name));
|
||||
}
|
||||
}
|
80
lib/actions-oclif/app/rm.ts
Normal file
80
lib/actions-oclif/app/rm.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an application.
|
||||
|
||||
Permanently remove a balena application.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app rm MyApp',
|
||||
'$ balena app rm MyApp --yes',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app rm <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppRmCmd,
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Confirm
|
||||
await patterns.confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete application ${params.name}?`,
|
||||
);
|
||||
|
||||
// Remove
|
||||
await getBalenaSdk().models.application.remove(tryAsInteger(params.name));
|
||||
}
|
||||
}
|
96
lib/actions-oclif/apps.ts
Normal file
96
lib/actions-oclif/apps.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface ExtendedApplication extends Application {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class AppsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all applications.
|
||||
|
||||
list all your balena applications.
|
||||
|
||||
For detailed information on a particular application,
|
||||
use \`balena app <name> instead\`.
|
||||
`;
|
||||
public static examples = ['$ balena apps'];
|
||||
|
||||
public static usage = 'apps';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description: isV12()
|
||||
? 'No-op since release v12.0.0'
|
||||
: 'add extra columns in the tabular output (SLUG)',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(AppsCmd);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications: ExtendedApplication[] = await balena.models.application.getAll(
|
||||
{
|
||||
$select: ['id', 'app_name', 'slug', 'device_type'],
|
||||
$expand: { owns__device: { $select: 'is_online' } },
|
||||
},
|
||||
);
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = _.size(application.owns__device);
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name',
|
||||
options.verbose || isV12() ? 'slug' : '',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
76
lib/actions-oclif/device/identify.ts
Normal file
76
lib/actions-oclif/device/identify.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceIdentifyCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Identify a device.
|
||||
|
||||
Identify a device by making the ACT LED blink (Raspberry Pi).
|
||||
`;
|
||||
public static examples = ['$ balena device identify 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to identify',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device identify <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
try {
|
||||
await balena.models.device.identify(params.uuid);
|
||||
} catch (e) {
|
||||
// Expected message: 'Request error: No online device(s) found'
|
||||
if (e.message?.toLowerCase().includes('online')) {
|
||||
throw new ExpectedError(`Device ${params.uuid} is not online`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
lib/actions-oclif/device/index.ts
Normal file
110
lib/actions-oclif/device/index.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Application, Device } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends Device {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
commit?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Show info about a single device.
|
||||
|
||||
Show information about a single device.
|
||||
`;
|
||||
public static examples = ['$ balena device 7cf02a6'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the device uuid',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device: ExtendedDevice = await balena.models.device.get(
|
||||
params.uuid,
|
||||
expandForAppName,
|
||||
);
|
||||
|
||||
const deviceStatus = await balena.models.device.getStatus(device.uuid);
|
||||
device.status = deviceStatus;
|
||||
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
|
||||
device.commit = device.is_on__commit;
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
'id',
|
||||
'device_type',
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'application_name',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
131
lib/actions-oclif/device/move.ts
Normal file
131
lib/actions-oclif/device/move.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application, Device } from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface ExtendedDevice extends Device {
|
||||
application_name?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceMoveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move a device to another application.
|
||||
|
||||
Move a device to another application.
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device move 7cf02a6',
|
||||
'$ balena device move 7cf02a6 --application MyNewApp',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to move',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device move <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceMoveCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const device: ExtendedDevice = await balena.models.device.get(
|
||||
params.uuid,
|
||||
expandForAppName,
|
||||
);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
|
||||
// Get destination application
|
||||
let application;
|
||||
if (options.application) {
|
||||
application = options.application;
|
||||
} else {
|
||||
const [deviceDeviceType, deviceTypes] = await Promise.all([
|
||||
balena.models.device.getManifestBySlug(device.device_type),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter(
|
||||
(dt) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
);
|
||||
|
||||
application = await patterns.selectApplication((app: Application) =>
|
||||
_.every([
|
||||
_.some(compatibleDeviceTypes, (dt) => dt.slug === app.device_type),
|
||||
// @ts-ignore using the extended device object prop
|
||||
device.application_name !== app.app_name,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
await balena.models.device.move(params.uuid, tryAsInteger(application));
|
||||
|
||||
console.info(`${params.uuid} was moved to ${application}`);
|
||||
}
|
||||
}
|
148
lib/actions-oclif/device/os-update.ts
Normal file
148
lib/actions-oclif/device/os-update.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Device } from 'balena-sdk';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
version?: string;
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceOsUpdateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Start a Host OS update for a device.
|
||||
|
||||
Start a Host OS update for a device.
|
||||
|
||||
Note this command will ask for confirmation interactively.
|
||||
This can be avoided by passing the \`--yes\` option.
|
||||
|
||||
Requires balenaCloud; will not work with openBalena or standalone balenaOS.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device os-update 23c73a1',
|
||||
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to update',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device os-update <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
version: flags.string({
|
||||
description: 'a balenaOS version',
|
||||
}),
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceOsUpdateCmd,
|
||||
);
|
||||
|
||||
const _ = await import('lodash');
|
||||
const sdk = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
const form = await import('resin-cli-form');
|
||||
|
||||
// Get device info
|
||||
const {
|
||||
uuid,
|
||||
device_type,
|
||||
os_version,
|
||||
os_variant,
|
||||
} = await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'device_type', 'os_version', 'os_variant'],
|
||||
});
|
||||
|
||||
// Get current device OS version
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
os_version,
|
||||
os_variant,
|
||||
} as Device);
|
||||
if (!currentOsVersion) {
|
||||
throw new ExpectedError(
|
||||
'The current os version of the device is not available',
|
||||
);
|
||||
}
|
||||
|
||||
// Get supported OS update versions
|
||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||
device_type,
|
||||
currentOsVersion,
|
||||
);
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
throw new ExpectedError(
|
||||
'There are no available Host OS update targets for this device',
|
||||
);
|
||||
}
|
||||
|
||||
// Get target OS version
|
||||
let targetOsVersion = options.version;
|
||||
if (targetOsVersion != null) {
|
||||
if (!_.includes(hupVersionInfo.versions, targetOsVersion)) {
|
||||
throw new ExpectedError(
|
||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
targetOsVersion = await form.ask({
|
||||
message: 'Target OS version',
|
||||
type: 'list',
|
||||
choices: hupVersionInfo.versions.map((version) => ({
|
||||
name:
|
||||
hupVersionInfo.recommended === version
|
||||
? `${version} (recommended)`
|
||||
: version,
|
||||
value: version,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm and start update
|
||||
await patterns.confirm(
|
||||
options.yes || false,
|
||||
'Host OS updates require a device restart when they complete. Are you sure you want to proceed?',
|
||||
);
|
||||
|
||||
await sdk.models.device.startOsUpdate(uuid, targetOsVersion);
|
||||
await patterns.awaitDeviceOsUpdate(uuid, targetOsVersion);
|
||||
}
|
||||
}
|
149
lib/actions-oclif/device/public-url.ts
Normal file
149
lib/actions-oclif/device/public-url.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
enable: boolean;
|
||||
disable: boolean;
|
||||
status: boolean;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
// Optional hidden arg to support old command format
|
||||
legacyUuid?: string;
|
||||
}
|
||||
|
||||
export default class DevicePublicUrlCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get or manage the public URL for a device.
|
||||
|
||||
This command will output the current public URL for the
|
||||
specified device. It can also enable or disable the URL,
|
||||
or output the enabled status, using the respective options.
|
||||
|
||||
The old command style 'balena device public-url enable <uuid>'
|
||||
is deprecated, but still supported.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device public-url 23c73a1',
|
||||
'$ balena device public-url 23c73a1 --enable',
|
||||
'$ balena device public-url 23c73a1 --disable',
|
||||
'$ balena device public-url 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to manage',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
// Optional hidden arg to support old command format
|
||||
name: 'legacyUuid',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device public-url <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
enable: flags.boolean({
|
||||
description: 'enable the public URL',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: flags.boolean({
|
||||
description: 'disable the public URL',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: flags.boolean({
|
||||
description: 'determine if public URL is enabled',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DevicePublicUrlCmd,
|
||||
);
|
||||
|
||||
// Legacy command format support.
|
||||
// Previously this command used the following format
|
||||
// (changed due to oclif technicalities):
|
||||
// `balena device public-url enable|disable|status <uuid>`
|
||||
if (params.legacyUuid) {
|
||||
const action = params.uuid;
|
||||
if (!['enable', 'disable', 'status'].includes(action)) {
|
||||
throw new ExpectedError(
|
||||
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
|
||||
);
|
||||
}
|
||||
|
||||
options.enable = action === 'enable';
|
||||
options.disable = action === 'disable';
|
||||
options.status = action === 'status';
|
||||
params.uuid = params.legacyUuid;
|
||||
delete params.legacyUuid;
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (options.enable) {
|
||||
// Enable public URL
|
||||
await balena.models.device.enableDeviceUrl(params.uuid);
|
||||
} else if (options.disable) {
|
||||
// Disable public URL
|
||||
await balena.models.device.disableDeviceUrl(params.uuid);
|
||||
} else if (options.status) {
|
||||
// Output bool indicating if public URL enabled
|
||||
const hasUrl = await balena.models.device.hasDeviceUrl(params.uuid);
|
||||
console.log(hasUrl);
|
||||
} else {
|
||||
// Output public URL
|
||||
try {
|
||||
const url = await balena.models.device.getDeviceUrl(params.uuid);
|
||||
console.log(url);
|
||||
} catch (e) {
|
||||
if (e.message.includes('Device is not web accessible')) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Public URL is not enabled for this device.
|
||||
|
||||
To enable, use:
|
||||
balena device public-url ${params.uuid} --enable
|
||||
`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
lib/actions-oclif/device/reboot.ts
Normal file
73
lib/actions-oclif/device/reboot.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
force: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceRebootCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart a device.
|
||||
|
||||
Remotely reboot a device.
|
||||
`;
|
||||
public static examples = ['$ balena device reboot 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to reboot',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device reboot <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
force: cf.force,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRebootCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// The SDK current throws "BalenaDeviceNotFound: Device not found: xxxxx"
|
||||
// when the device is not online, which may be confusing.
|
||||
// https://github.com/balena-io/balena-cli/issues/1872
|
||||
await balena.models.device.reboot(params.uuid, options);
|
||||
}
|
||||
}
|
83
lib/actions-oclif/device/register.ts
Normal file
83
lib/actions-oclif/device/register.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
uuid?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
}
|
||||
|
||||
export default class DeviceRegisterCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Register a device.
|
||||
|
||||
Register a device to an application.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device register MyApp',
|
||||
'$ balena device register MyApp --uuid <uuid>',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'application',
|
||||
description: 'the name or id of application to register device with',
|
||||
parse: (app) => tryAsInteger(app),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device register <application>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
uuid: flags.string({
|
||||
description: 'custom uuid',
|
||||
char: 'u',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRegisterCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = await balena.models.application.get(params.application);
|
||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||
|
||||
console.info(`Registering to ${application.app_name}: ${uuid}`);
|
||||
|
||||
const result = await balena.models.device.register(application.id, uuid);
|
||||
|
||||
return result && result.uuid;
|
||||
}
|
||||
}
|
85
lib/actions-oclif/device/rename.ts
Normal file
85
lib/actions-oclif/device/rename.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class DeviceRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename a device.
|
||||
|
||||
Rename a device.
|
||||
|
||||
Note, if the name is omitted, it will be prompted for interactively.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device rename 7cf02a6',
|
||||
'$ balena device rename 7cf02a6 MyPi',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to rename',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the device',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device rename <uuid> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const form = await import('resin-cli-form');
|
||||
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await form.ask({
|
||||
message: 'How do you want to name this device?',
|
||||
type: 'input',
|
||||
})) ||
|
||||
'';
|
||||
|
||||
await balena.models.device.rename(params.uuid, newName);
|
||||
}
|
||||
}
|
84
lib/actions-oclif/device/rm.ts
Normal file
84
lib/actions-oclif/device/rm.ts
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a device.
|
||||
|
||||
Remove a device from balena.
|
||||
|
||||
Note this command asks for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` option.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device rm 7cf02a6',
|
||||
'$ balena device rm 7cf02a6 --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to remove',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device rm <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRmCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
// Confirm
|
||||
await patterns.confirm(
|
||||
options.yes,
|
||||
'Are you sure you want to delete the device?',
|
||||
);
|
||||
|
||||
// Remove
|
||||
await balena.models.device.remove(params.uuid);
|
||||
}
|
||||
}
|
80
lib/actions-oclif/device/shutdown.ts
Normal file
80
lib/actions-oclif/device/shutdown.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
force: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceShutdownCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Shutdown a device.
|
||||
|
||||
Remotely shutdown a device.
|
||||
`;
|
||||
public static examples = ['$ balena device shutdown 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
description: 'the uuid of the device to shutdown',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'device shutdown <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
force: cf.force,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceShutdownCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
try {
|
||||
await balena.models.device.shutdown(params.uuid, options);
|
||||
} catch (e) {
|
||||
// Expected message: 'Request error: No online device(s) found'
|
||||
if (e.message?.toLowerCase().includes('online')) {
|
||||
throw new ExpectedError(`Device ${params.uuid} is not online`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
lib/actions-oclif/devices/index.ts
Normal file
113
lib/actions-oclif/devices/index.ts
Normal file
@ -0,0 +1,113 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Device, Application } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends Device {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
|
||||
list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
'$ balena devices --application MyApp',
|
||||
'$ balena devices --app MyApp',
|
||||
'$ balena devices -a MyApp',
|
||||
];
|
||||
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
let devices: ExtendedDevice[];
|
||||
|
||||
if (options.application != null) {
|
||||
devices = await balena.models.device.getAllByApplication(
|
||||
tryAsInteger(options.application),
|
||||
expandForAppName,
|
||||
);
|
||||
} else {
|
||||
devices = await balena.models.device.getAll(expandForAppName);
|
||||
}
|
||||
|
||||
devices = _.map(devices, function (device) {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
|
||||
device.uuid = device.uuid.slice(0, 7);
|
||||
return device;
|
||||
});
|
||||
|
||||
console.log(
|
||||
getVisuals().table.horizontal(devices, [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
117
lib/actions-oclif/devices/supported.ts
Normal file
117
lib/actions-oclif/devices/supported.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { isV12 } from '../../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
discontinued: boolean;
|
||||
help: void;
|
||||
json?: boolean;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export default class DevicesSupportedCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||
|
||||
The --verbose option adds extra columns/fields to the output, including the
|
||||
"STATE" column whose values are one of 'beta', 'released' or 'discontinued'.
|
||||
However, 'discontinued' device types are only listed if the '--discontinued'
|
||||
option is used.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings (for example, the ALIASES column contains a
|
||||
list of zero or more values). The 'jq' utility may be helpful in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices supported',
|
||||
'$ balena devices supported --verbose',
|
||||
'$ balena devices supported -vj',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'devices supported ' +
|
||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
discontinued: flags.boolean({
|
||||
description: 'include "discontinued" device types',
|
||||
}),
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'add extra columns in the tabular output (ALIASES, ARCH, STATE)',
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
||||
let deviceTypes: Array<Partial<SDK.DeviceType>> = await getBalenaSdk()
|
||||
.models.config.getDeviceTypes()
|
||||
.map((d) => {
|
||||
if (d.aliases && d.aliases.length) {
|
||||
// remove aliases that are equal to the slug
|
||||
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
|
||||
if (!options.json) {
|
||||
// stringify the aliases array with commas and spaces
|
||||
d.aliases = [d.aliases.join(', ')];
|
||||
}
|
||||
} else {
|
||||
// ensure it is always an array (for the benefit of JSON output)
|
||||
d.aliases = [];
|
||||
}
|
||||
return d;
|
||||
});
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
|
||||
}
|
||||
const fields = options.verbose
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: isV12()
|
||||
? ['slug', 'aliases', 'arch', 'name']
|
||||
: ['slug', 'name'];
|
||||
deviceTypes = _.sortBy(
|
||||
deviceTypes.map((d) => _.pick(d, fields) as Partial<SDK.DeviceType>),
|
||||
fields,
|
||||
);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
const visuals = getVisuals();
|
||||
const output = await visuals.table.horizontal(deviceTypes, fields);
|
||||
console.log(output);
|
||||
}
|
||||
}
|
||||
}
|
241
lib/actions-oclif/env/add.ts
vendored
Normal file
241
lib/actions-oclif/env/add.ts
vendored
Normal file
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @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 { flags } from '@oclif/command';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string; // application name
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
service?: string; // service name
|
||||
}
|
||||
|
||||
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, device or service.
|
||||
|
||||
Add an environment or config variable to an application, device or service,
|
||||
as selected by the respective command-line options. Either the --application
|
||||
or the --device option must be provided, and either may be be used alongside
|
||||
the --service option to define a service-specific variable. (A service is an
|
||||
application container in a "microservices" application.) When the --service
|
||||
option is used in conjunction with the --device option, the service variable
|
||||
applies to the selected device only. Otherwise, it applies to all devices of
|
||||
the selected application (i.e., the application's fleet). If the --service
|
||||
option is omitted, the variable applies to all services.
|
||||
|
||||
If VALUE is omitted, the CLI will attempt to use the value of the environment
|
||||
variable of same name in the CLI process' environment. In this case, a warning
|
||||
message will be printed. Use \`--quiet\` to suppress it.
|
||||
|
||||
'BALENA_' or 'RESIN_' are reserved variable name prefixes used to identify
|
||||
"configuration variables". Configuration variables control balena platform
|
||||
features and are treated specially by balenaOS and the balena supervisor
|
||||
running on devices. They are also stored differently in the balenaCloud API
|
||||
database. Configuration variables cannot be set for specific services,
|
||||
therefore the --service option cannot be used when the variable name starts
|
||||
with a reserved prefix. When defining custom application variables, please
|
||||
avoid the reserved prefixes.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp --service MyService',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
required: true,
|
||||
description: 'environment or config variable name',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env add' to avoid oclif's 'env:add' topic syntax
|
||||
public static usage =
|
||||
'env add ' + new CommandHelp({ args: EnvAddCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvAddCmd,
|
||||
);
|
||||
const cmd = this;
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --application or the --device option must always be used',
|
||||
);
|
||||
}
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = process.env[params.name];
|
||||
|
||||
if (params.value == null) {
|
||||
throw new Error(
|
||||
`Value not found for environment variable: ${params.name}`,
|
||||
);
|
||||
} else if (!options.quiet) {
|
||||
cmd.warn(
|
||||
`Using ${params.name}=${params.value} from CLI process environment`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const reservedPrefixes = await getReservedPrefixes(balena);
|
||||
const isConfigVar = _.some(reservedPrefixes, (prefix) =>
|
||||
_.startsWith(params.name, prefix),
|
||||
);
|
||||
|
||||
if (options.service) {
|
||||
if (isConfigVar) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Configuration variables prefixed with "${reservedPrefixes.join(
|
||||
'" or "',
|
||||
)}" cannot be set per service.
|
||||
Hint: remove the --service option or rename the variable.
|
||||
`);
|
||||
}
|
||||
await setServiceVars(balena, params, options);
|
||||
return;
|
||||
}
|
||||
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
if (options.application) {
|
||||
await balena.models.application[varType].set(
|
||||
options.application,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} else if (options.device) {
|
||||
await balena.models.device[varType].set(
|
||||
options.device,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service variables for a device or application.
|
||||
*/
|
||||
async function setServiceVars(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
params: ArgsDef,
|
||||
options: FlagsDef,
|
||||
) {
|
||||
if (options.application) {
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
options.application,
|
||||
options.service!,
|
||||
);
|
||||
await sdk.models.service.var.set(serviceId, params.name, params.value!);
|
||||
} else {
|
||||
const { getDeviceAndAppFromUUID } = await import('../../utils/cloud');
|
||||
const [device, app] = await getDeviceAndAppFromUUID(
|
||||
sdk,
|
||||
options.device!,
|
||||
['id'],
|
||||
['app_name'],
|
||||
);
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
app.app_name,
|
||||
options.service!,
|
||||
);
|
||||
await sdk.models.device.serviceVar.set(
|
||||
device.id,
|
||||
serviceId,
|
||||
params.name,
|
||||
params.value!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a sevice ID for the given app name and service name.
|
||||
*/
|
||||
async function getServiceIdForApp(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
appName: string,
|
||||
serviceName: string,
|
||||
): Promise<number> {
|
||||
let serviceId: number | undefined;
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (!_.isEmpty(services)) {
|
||||
serviceId = services[0].id;
|
||||
}
|
||||
if (serviceId === undefined) {
|
||||
throw new ExpectedError(
|
||||
`Cannot find service ${serviceName} for application ${appName}`,
|
||||
);
|
||||
}
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of variable name prefixes like: [ 'RESIN_', 'BALENA_' ].
|
||||
* These prefixes can be used to identify "configuration variables".
|
||||
*/
|
||||
async function getReservedPrefixes(
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
): Promise<string[]> {
|
||||
const settings = await balena.settings.getAll();
|
||||
const response = await balena.request.send({
|
||||
baseUrl: settings.apiUrl,
|
||||
url: '/config/vars',
|
||||
});
|
||||
|
||||
return response.body.reservedNamespaces;
|
||||
}
|
100
lib/actions-oclif/env/rename.ts
vendored
Normal file
100
lib/actions-oclif/env/rename.ts
vendored
Normal file
@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
config: boolean;
|
||||
device: boolean;
|
||||
service: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class EnvRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Change the value of a config or env var for an app, device or service.
|
||||
|
||||
Change the value of a configuration or environment variable for an application,
|
||||
device or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env rename 123123 emacs',
|
||||
'$ balena env rename 234234 emacs --service',
|
||||
'$ balena env rename 345345 emacs --device',
|
||||
'$ balena env rename 456456 emacs --device --service',
|
||||
'$ balena env rename 567567 1 --config',
|
||||
'$ balena env rename 678678 1 --device --config',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
required: true,
|
||||
description:
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env rename' to avoid oclif's 'env:rename' topic syntax
|
||||
public static usage =
|
||||
'env rename ' + new CommandHelp({ args: EnvRenameCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRenameCmd,
|
||||
);
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
await getBalenaSdk().pine.patch({
|
||||
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
||||
id: params.id,
|
||||
body: {
|
||||
value: params.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
108
lib/actions-oclif/env/rm.ts
vendored
Normal file
108
lib/actions-oclif/env/rm.ts
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* @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 { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
config: boolean;
|
||||
device: boolean;
|
||||
service: boolean;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class EnvRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a config or env var from an application, device or service.
|
||||
|
||||
Remove a configuration or environment variable from an application, device
|
||||
or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
|
||||
Interactive confirmation is normally asked before the variable is deleted.
|
||||
The --yes option disables this behavior.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena env rm 123123',
|
||||
'$ balena env rm 234234 --yes',
|
||||
'$ balena env rm 345345 --config',
|
||||
'$ balena env rm 456456 --service',
|
||||
'$ balena env rm 567567 --device',
|
||||
'$ balena env rm 678678 --device --config',
|
||||
'$ balena env rm 789789 --device --service --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'env rm' to avoid oclif's 'env:rm' topic syntax
|
||||
public static usage =
|
||||
'env rm ' + new CommandHelp({ args: EnvRmCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
yes: flags.boolean({
|
||||
char: 'y',
|
||||
description:
|
||||
'do not prompt for confirmation before deleting the variable',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRmCmd,
|
||||
);
|
||||
const balena = getBalenaSdk();
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
await confirm(
|
||||
opt.yes || false,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
await balena.pine.delete({
|
||||
resource: ec.getVarResourceName(opt.config, opt.device, opt.service),
|
||||
id: params.id,
|
||||
});
|
||||
}
|
||||
}
|
445
lib/actions-oclif/envs.ts
Normal file
445
lib/actions-oclif/envs.ts
Normal file
@ -0,0 +1,445 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../command';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { CommandHelp } from '../utils/oclif-utils';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
all?: boolean; // whether to include application-wide, device-wide variables //TODO: REMOVE
|
||||
application?: string; // application name
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
verbose: boolean;
|
||||
}
|
||||
|
||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||
appName?: string | null; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface DeviceServiceEnvironmentVariableInfo
|
||||
extends SDK.DeviceServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface ServiceEnvironmentVariableInfo
|
||||
extends SDK.ServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
export default class EnvsCmd extends Command {
|
||||
public static description = isV12()
|
||||
? stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
|
||||
List the environment or configuration variables of an application, device or
|
||||
service, as selected by the respective command-line options. (A service is
|
||||
an application container in a "microservices" application.)
|
||||
|
||||
The results include application-wide (fleet), device-wide (multiple services on
|
||||
a device) and service-specific variables that apply to the selected application,
|
||||
device or service. It can be thought of as including "inherited" variables;
|
||||
for example, a service inherits device-wide variables, and a device inherits
|
||||
application-wide variables.
|
||||
|
||||
The printed output may include DEVICE and/or SERVICE columns to distinguish
|
||||
between application-wide, device-specific and service-specific variables.
|
||||
An asterisk in these columns indicates that the variable applies to
|
||||
"all devices" or "all services".
|
||||
|
||||
The --config option is used to list "configuration variables" that control
|
||||
balena platform features, as opposed to custom environment variables defined
|
||||
by the user. The --config and the --service options are mutually exclusive
|
||||
because configuration variables cannot be set for specific services.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings. The 'jq' utility may be helpful in shell
|
||||
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
|
||||
JSON array ([]) is printed instead of an error message when no variables exist
|
||||
for the given query. When querying variables for a device, note that the
|
||||
application name may be null in JSON output (or 'N/A' in tabular output) if the
|
||||
application linked to the device is no longer accessible by the current user
|
||||
(for example, in case the current user has been removed from the application
|
||||
by its owner).
|
||||
`
|
||||
: stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
|
||||
List the environment or configuration variables of an application, device or
|
||||
service, as selected by the respective command-line options. (A service is
|
||||
an application container in a "microservices" application.)
|
||||
|
||||
The --config option is used to list "configuration variables" that control
|
||||
balena platform features, as opposed to custom environment variables defined
|
||||
by the user. The --config and the --service options are mutually exclusive
|
||||
because configuration variables cannot be set for specific services.
|
||||
|
||||
The --all option is used to include application-wide (fleet), device-wide
|
||||
(multiple services on a device) and service-specific variables that apply to
|
||||
the selected application, device or service. It can be thought of as including
|
||||
"inherited" variables: for example, a service inherits device-wide variables,
|
||||
and a device inherits application-wide variables. Variables are still filtered
|
||||
out by type with the --config option, such that configuration and non-
|
||||
configuration variables are never listed together.
|
||||
|
||||
When the --all option is used, the printed output may include DEVICE and/or
|
||||
SERVICE columns to distinguish between application-wide, device-specific and
|
||||
service-specific variables. An asterisk in these columns indicates that the
|
||||
variable applies to "all devices" or "all services".
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents data
|
||||
types like lists and empty strings. The 'jq' utility may be helpful in shell
|
||||
scripts (https://stedolan.github.io/jq/manual/). When --json is used, an empty
|
||||
JSON array ([]) is printed instead of an error message when no variables exist
|
||||
for the given query. When querying variables for a device, note that the
|
||||
application name may be null in JSON output (or 'N/A' in tabular output) if the
|
||||
application linked to the device is no longer accessible by the current user
|
||||
(for example, in case the current user has been removed from the application
|
||||
by its owner).
|
||||
`;
|
||||
public static examples = isV12()
|
||||
? [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --json',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --json',
|
||||
'$ balena envs --device 7cf02a6 --config --json',
|
||||
'$ balena envs --device 7cf02a6 --service MyService',
|
||||
]
|
||||
: [
|
||||
'$ balena envs --application MyApp',
|
||||
'$ balena envs --application MyApp --all --json',
|
||||
'$ balena envs --application MyApp --service MyService',
|
||||
'$ balena envs --application MyApp --all --service MyService',
|
||||
'$ balena envs --application MyApp --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --all --json',
|
||||
'$ balena envs --device 7cf02a6 --config --all --json',
|
||||
'$ balena envs --device 7cf02a6 --all --service MyService',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'envs ' + new CommandHelp({ args: EnvsCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV12()
|
||||
? {
|
||||
all: flags.boolean({
|
||||
description: stripIndent`
|
||||
No-op since balena CLI v12.0.0.`,
|
||||
hidden: true,
|
||||
}),
|
||||
}
|
||||
: {
|
||||
all: flags.boolean({
|
||||
description: stripIndent`
|
||||
include app-wide, device-wide variables that apply to the selected device or service.
|
||||
Variables are still filtered out by type with the --config option.`,
|
||||
}),
|
||||
}),
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
config: flags.boolean({
|
||||
char: 'c',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
options.all = options.all || isV12();
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError('You must specify an application or device');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
|
||||
|
||||
let appName = options.application;
|
||||
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||
|
||||
if (options.device) {
|
||||
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appName = app.app_name;
|
||||
}
|
||||
}
|
||||
if (appName && options.service) {
|
||||
await validateServiceName(balena, options.service, appName);
|
||||
}
|
||||
if (options.application || options.all) {
|
||||
variables.push(...(await getAppVars(balena, appName, options)));
|
||||
}
|
||||
if (fullUUID) {
|
||||
variables.push(
|
||||
...(await getDeviceVars(balena, fullUUID, appName, options)),
|
||||
);
|
||||
}
|
||||
if (!options.json && _.isEmpty(variables)) {
|
||||
const target =
|
||||
(options.service ? `service "${options.service}" of ` : '') +
|
||||
(options.application
|
||||
? `application "${options.application}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
|
||||
await this.printVariables(variables, options);
|
||||
}
|
||||
|
||||
protected async printVariables(
|
||||
varArray: EnvironmentVariableInfo[],
|
||||
options: FlagsDef,
|
||||
) {
|
||||
const fields = ['id', 'name', 'value'];
|
||||
|
||||
if (options.all) {
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = _.map(varArray, (i: EnvironmentVariableInfo) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
return i;
|
||||
});
|
||||
|
||||
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
if (!options.config) {
|
||||
fields.push(options.json ? 'serviceName' : 'serviceName => SERVICE');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
this.log(
|
||||
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
|
||||
);
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateServiceName(
|
||||
sdk: SDK.BalenaSDK,
|
||||
serviceName: string,
|
||||
appName: string,
|
||||
) {
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (_.isEmpty(services)) {
|
||||
throw new ExpectedError(
|
||||
`Service "${serviceName}" not found for application "${appName}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch application-wide config / env / service vars.
|
||||
* If options.application is undefined, an attempt is made to obtain the
|
||||
* application name from the device UUID (options.device). If this attempt
|
||||
* fails because the device does not belong to any application, an emtpy
|
||||
* array is returned.
|
||||
*/
|
||||
async function getAppVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
appName: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const appVars: EnvironmentVariableInfo[] = [];
|
||||
if (!appName) {
|
||||
return appVars;
|
||||
}
|
||||
if (options.config || options.all || !options.service) {
|
||||
const vars = await sdk.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(appName);
|
||||
fillInInfoFields(vars, appName);
|
||||
appVars.push(...vars);
|
||||
}
|
||||
if (!options.config && (options.service || options.all)) {
|
||||
const pineOpts: SDK.PineOptionsFor<SDK.ServiceEnvironmentVariable> = {
|
||||
$expand: {
|
||||
service: {},
|
||||
},
|
||||
};
|
||||
if (options.service) {
|
||||
pineOpts.$filter = {
|
||||
service: {
|
||||
service_name: options.service,
|
||||
},
|
||||
};
|
||||
}
|
||||
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
||||
appName,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(serviceVars, appName);
|
||||
appVars.push(...serviceVars);
|
||||
}
|
||||
return appVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch config / env / service vars when the '--device' option is provided.
|
||||
* Precondition: options.device must be defined.
|
||||
*/
|
||||
async function getDeviceVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
fullUUID: string,
|
||||
appName: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const printedUUID = options.json ? fullUUID : options.device!;
|
||||
const deviceVars: EnvironmentVariableInfo[] = [];
|
||||
if (options.config) {
|
||||
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceConfigVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceConfigVars);
|
||||
} else {
|
||||
if (options.service || options.all) {
|
||||
const pineOpts: SDK.PineOptionsFor<SDK.DeviceServiceEnvironmentVariable> = {
|
||||
$expand: {
|
||||
service_install: {
|
||||
$expand: 'installs__service',
|
||||
},
|
||||
},
|
||||
};
|
||||
if (options.service) {
|
||||
pineOpts.$filter = {
|
||||
service_install: {
|
||||
installs__service: { service_name: options.service },
|
||||
},
|
||||
};
|
||||
}
|
||||
const deviceServiceVars = await sdk.models.device.serviceVar.getAllByDevice(
|
||||
fullUUID,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(deviceServiceVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceServiceVars);
|
||||
}
|
||||
if (!options.service || options.all) {
|
||||
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceEnvVars, appName, printedUUID);
|
||||
deviceVars.push(...deviceEnvVars);
|
||||
}
|
||||
}
|
||||
return deviceVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* For each env var object in varArray, fill in its top-level serviceName
|
||||
* and deviceUUID fields. An asterisk is used to indicate that the variable
|
||||
* applies to "all services" or "all devices".
|
||||
*/
|
||||
function fillInInfoFields(
|
||||
varArray:
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appName?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
if ('service' in envVar) {
|
||||
// envVar is of type ServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = _.at(envVar as any, 'service[0].service_name')[0];
|
||||
} else if ('service_install' in envVar) {
|
||||
// envVar is of type DeviceServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = _.at(
|
||||
envVar as any,
|
||||
'service_install[0].installs__service[0].service_name',
|
||||
)[0];
|
||||
}
|
||||
envVar.appName = appName;
|
||||
envVar.serviceName = envVar.serviceName || '*';
|
||||
envVar.deviceUUID = deviceUUID || '*';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform each object (item) of varArray to preserve only the
|
||||
* fields (keys) listed in the fields argument.
|
||||
*/
|
||||
function stringifyVarArray<T = Dictionary<any>>(
|
||||
varArray: T[],
|
||||
fields: string[],
|
||||
): string {
|
||||
const transformed = _.map(varArray, (o: Dictionary<any>) =>
|
||||
_.transform(
|
||||
o,
|
||||
(result, value, key) => {
|
||||
if (fields.includes(key)) {
|
||||
result[key] = value;
|
||||
}
|
||||
},
|
||||
{} as Dictionary<any>,
|
||||
),
|
||||
);
|
||||
return JSON.stringify(transformed, null, 4);
|
||||
}
|
81
lib/actions-oclif/internal/osinit.ts
Normal file
81
lib/actions-oclif/internal/osinit.ts
Normal file
@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
// 'Internal' commands are called during the execution of other commands.
|
||||
// `osinit` is called during `os initialize`
|
||||
// TODO: These should be refactored to modules/functions, and removed
|
||||
// See previous `internal sudo` refactor:
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455/files
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
type: string;
|
||||
config: string;
|
||||
}
|
||||
|
||||
export default class OsinitCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Do actual init of the device with the preconfigured os image.
|
||||
|
||||
Don't use this command directly!
|
||||
Use \`balena os initialize <image>\` instead.
|
||||
`;
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'config',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
'internal osinit ' +
|
||||
new CommandHelp({ args: OsinitCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static hidden = true;
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
|
||||
|
||||
const { initialize } = await import('balena-device-init');
|
||||
const { getManifest, osProgressHandler } = await import(
|
||||
'../../utils/helpers'
|
||||
);
|
||||
|
||||
const config = JSON.parse(params.config);
|
||||
const manifest = await getManifest(params.image, params.type);
|
||||
|
||||
const initializeEmitter = await initialize(params.image, manifest, config);
|
||||
await osProgressHandler(initializeEmitter);
|
||||
}
|
||||
}
|
46
lib/actions-oclif/internal/scandevices.ts
Normal file
46
lib/actions-oclif/internal/scandevices.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
|
||||
// 'Internal' commands are called during the execution of other commands.
|
||||
// `scandevices` is called during by `join`,`leave'.
|
||||
// TODO: These should be refactored to modules/functions, and removed
|
||||
// See previous `internal sudo` refactor:
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455/files
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||
|
||||
export default class ScandevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for local balena-enabled devices and show a picker to choose one.
|
||||
|
||||
Don't use this command directly!
|
||||
`;
|
||||
|
||||
public static usage = 'internal scandevices';
|
||||
|
||||
public static root = true;
|
||||
public static hidden = true;
|
||||
|
||||
public async run() {
|
||||
const { forms } = await import('balena-sync');
|
||||
const hostnameOrIp = await forms.selectLocalBalenaOsDevice();
|
||||
return console.error(`==> Selected device: ${hostnameOrIp}`);
|
||||
}
|
||||
}
|
98
lib/actions-oclif/join.ts
Normal file
98
lib/actions-oclif/join.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
deviceIpOrHostname?: string;
|
||||
}
|
||||
|
||||
export default class JoinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move a local device to an application on another balena server.
|
||||
|
||||
Move a local device to an application on another balena server, causing
|
||||
the device to "join" the new server. The device must be running balenaOS.
|
||||
|
||||
For example, you could provision a device against an openBalena installation
|
||||
where you perform end-to-end tests and then move it to balenaCloud when it's
|
||||
ready for production.
|
||||
|
||||
To move a device between applications on the same server, use the
|
||||
\`balena device move\` command instead of \`balena join\`.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This requires root privileges. Likewise, if
|
||||
the application flag is not provided then a picker will be shown.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena join',
|
||||
'$ balena join balena.local',
|
||||
'$ balena join balena.local --application MyApp',
|
||||
'$ balena join 192.168.1.25',
|
||||
'$ balena join 192.168.1.25 --application MyApp',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the IP or hostname of device',
|
||||
},
|
||||
];
|
||||
|
||||
// Hardcoded to preserve camelcase
|
||||
public static usage = 'join [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
description: 'the name of the application the device should join',
|
||||
...cf.application,
|
||||
},
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
JoinCmd,
|
||||
);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
return promote.join(
|
||||
logger,
|
||||
sdk,
|
||||
params.deviceIpOrHostname,
|
||||
options.application,
|
||||
);
|
||||
}
|
||||
}
|
86
lib/actions-oclif/key/add.ts
Normal file
86
lib/actions-oclif/key/add.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export default class KeyAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add an SSH key to balenaCloud.
|
||||
|
||||
Register an SSH in balenaCloud for the logged in user.
|
||||
|
||||
If \`path\` is omitted, the command will attempt
|
||||
to read the SSH key from stdin.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena key add Main ~/.ssh/id_rsa.pub',
|
||||
'$ cat ~/.ssh/id_rsa.pub | balena key add Main',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'the SSH key name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: `path`,
|
||||
description: `the path to the public key file`,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key add <name> [path]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static readStdin = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
|
||||
|
||||
let key: string;
|
||||
if (params.path != null) {
|
||||
const { readFile } = (await import('fs')).promises;
|
||||
key = await readFile(params.path, 'utf8');
|
||||
} else if (this.stdin.length > 0) {
|
||||
key = this.stdin;
|
||||
} else {
|
||||
throw new ExpectedError('No public key file or path provided.');
|
||||
}
|
||||
|
||||
await getBalenaSdk().models.key.create(params.name, key);
|
||||
}
|
||||
}
|
79
lib/actions-oclif/key/index.ts
Normal file
79
lib/actions-oclif/key/index.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class KeyCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display an SSH key.
|
||||
|
||||
Display a single SSH key registered in balenaCloud for the logged in user.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena key 17'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
||||
|
||||
const key = await getBalenaSdk().models.key.get(params.id);
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKey = {
|
||||
id: key.id,
|
||||
name: key.title,
|
||||
};
|
||||
|
||||
console.log(getVisuals().table.vertical(displayKey, ['id', 'name']));
|
||||
|
||||
// Since the public key string is long, it might
|
||||
// wrap to lines below, causing the table layout to break.
|
||||
// See https://github.com/balena-io/balena-cli/issues/151
|
||||
console.log('\n' + key.public_key);
|
||||
}
|
||||
}
|
79
lib/actions-oclif/key/rm.ts
Normal file
79
lib/actions-oclif/key/rm.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { parseAsInteger } from '../../utils/validation';
|
||||
|
||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default class KeyRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an SSH key from balenaCloud.
|
||||
|
||||
Remove a single SSH key registered in balenaCloud for the logged in user.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
description: 'balenaCloud ID for the SSH key',
|
||||
parse: (x) => parseAsInteger(x, 'id'),
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'key rm <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
KeyRmCmd,
|
||||
);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
||||
await patterns.confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete key ${params.id}?`,
|
||||
);
|
||||
|
||||
await getBalenaSdk().models.key.remove(params.id);
|
||||
}
|
||||
}
|
56
lib/actions-oclif/keys.ts
Normal file
56
lib/actions-oclif/keys.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class KeysCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the SSH keys in balenaCloud.
|
||||
|
||||
List all SSH keys registered in balenaCloud for the logged in user.
|
||||
`;
|
||||
public static examples = ['$ balena keys'];
|
||||
|
||||
public static usage = 'keys';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(KeysCmd);
|
||||
|
||||
const keys = await getBalenaSdk().models.key.getAll();
|
||||
|
||||
// Use 'name' instead of 'title' to match dashboard.
|
||||
const displayKeys: Array<{ id: number; name: string }> = keys.map((k) => {
|
||||
return { id: k.id, name: k.title };
|
||||
});
|
||||
|
||||
console.log(getVisuals().table.horizontal(displayKeys, ['id', 'name']));
|
||||
}
|
||||
}
|
80
lib/actions-oclif/leave.ts
Normal file
80
lib/actions-oclif/leave.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help?: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
deviceIpOrHostname?: string;
|
||||
}
|
||||
|
||||
export default class LeaveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a local device from its balena application.
|
||||
|
||||
Remove a local device from its balena application, causing the device to
|
||||
"leave" the server it is provisioned on. This effectively makes the device
|
||||
"unmanaged". The device must be running balenaOS.
|
||||
|
||||
The device entry on the server is preserved after running this command,
|
||||
so the device can subsequently re-join the server if needed.
|
||||
|
||||
If you don't specify a device hostname or IP, this command will automatically
|
||||
scan the local network for balenaOS devices and prompt you to select one
|
||||
from an interactive picker. This usually requires root privileges.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena leave',
|
||||
'$ balena leave balena.local',
|
||||
'$ balena leave 192.168.1.25',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'deviceIpOrHostname',
|
||||
description: 'the device IP or hostname',
|
||||
},
|
||||
];
|
||||
|
||||
// Hardcoded to preserve camelcase
|
||||
public static usage = 'leave [deviceIpOrHostname]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
|
||||
|
||||
const Logger = await import('../utils/logger');
|
||||
const promote = await import('../utils/promote');
|
||||
const sdk = getBalenaSdk();
|
||||
const logger = Logger.getLogger();
|
||||
return promote.leave(logger, sdk, params.deviceIpOrHostname);
|
||||
}
|
||||
}
|
188
lib/actions-oclif/login.ts
Normal file
188
lib/actions-oclif/login.ts
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import { ExpectedError } from '../errors';
|
||||
|
||||
interface FlagsDef {
|
||||
token: boolean;
|
||||
web: boolean;
|
||||
credentials: boolean;
|
||||
email?: string;
|
||||
user?: string;
|
||||
password?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export default class LoginCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Login to balena.
|
||||
|
||||
Login to your balena account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
- Web authorization: open your web browser and prompt to authorize the CLI
|
||||
from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using a session token or API key from the preferences page.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena login',
|
||||
'$ balena login --web',
|
||||
'$ balena login --token "..."',
|
||||
'$ balena login --credentials',
|
||||
'$ balena login --credentials --email johndoe@gmail.com --password secret',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
// Capitano allowed -t to be type boolean|string, which oclif does not.
|
||||
// So -t is now bool, and we check first arg for token content.
|
||||
name: 'token',
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'login';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
web: flags.boolean({
|
||||
char: 'w',
|
||||
description: 'web-based login',
|
||||
}),
|
||||
token: flags.boolean({
|
||||
char: 't',
|
||||
description: 'session token or API key',
|
||||
}),
|
||||
credentials: flags.boolean({
|
||||
char: 'c',
|
||||
description: 'credential-based login',
|
||||
}),
|
||||
email: flags.string({
|
||||
char: 'e',
|
||||
description: 'email',
|
||||
exclusive: ['user'],
|
||||
dependsOn: ['credentials'],
|
||||
}),
|
||||
// Capitano version of this command had a second alias for email, 'u'.
|
||||
// Using an oclif hidden flag to support the same behaviour.
|
||||
user: flags.string({
|
||||
char: 'u',
|
||||
hidden: true,
|
||||
exclusive: ['email'],
|
||||
dependsOn: ['credentials'],
|
||||
}),
|
||||
password: flags.string({
|
||||
char: 'p',
|
||||
description: 'password',
|
||||
dependsOn: ['credentials'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options, args: params } = this.parse<FlagsDef, ArgsDef>(
|
||||
LoginCmd,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const messages = await import('../utils/messages');
|
||||
const balenaUrl = await balena.settings.get('balenaUrl');
|
||||
|
||||
// Consolidate user/email options
|
||||
if (options.user != null) {
|
||||
options.email = options.user;
|
||||
}
|
||||
|
||||
console.log(messages.balenaAsciiArt);
|
||||
console.log(`\nLogging in to ${balenaUrl}`);
|
||||
await this.doLogin(options, params.token);
|
||||
const username = await balena.auth.whoami();
|
||||
|
||||
console.info(`Successfully logged in as: ${username}`);
|
||||
console.info(`\
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ balena help
|
||||
|
||||
${messages.reachingOut}`);
|
||||
|
||||
if (options.web) {
|
||||
const { shutdownServer } = await import('../auth');
|
||||
shutdownServer();
|
||||
}
|
||||
}
|
||||
|
||||
async doLogin(loginOptions: FlagsDef, token?: string): Promise<void> {
|
||||
const patterns = await import('../utils/patterns');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Token
|
||||
if (loginOptions.token) {
|
||||
if (!token) {
|
||||
const form = await import('resin-cli-form');
|
||||
token = await form.ask({
|
||||
message: 'Session token or API key from the preferences page',
|
||||
name: 'token',
|
||||
type: 'input',
|
||||
});
|
||||
}
|
||||
await balena.auth.loginWithToken(token!);
|
||||
if (!(await balena.auth.whoami())) {
|
||||
throw new ExpectedError('Token authentication failed');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Credentials
|
||||
else if (loginOptions.credentials) {
|
||||
return patterns.authenticate(loginOptions);
|
||||
}
|
||||
// Web
|
||||
else if (loginOptions.web) {
|
||||
const auth = await import('../auth');
|
||||
await auth.login();
|
||||
return;
|
||||
}
|
||||
|
||||
// User had not selected login preference, prompt interactively
|
||||
const loginType = await patterns.askLoginType();
|
||||
if (loginType === 'register') {
|
||||
const signupUrl = 'https://dashboard.balena-cloud.com/signup';
|
||||
const open = await import('open');
|
||||
open(signupUrl, { wait: false });
|
||||
throw new ExpectedError(`Please sign up at ${signupUrl}`);
|
||||
}
|
||||
|
||||
// Set login options flag from askLoginType, and run again
|
||||
loginOptions[loginType] = true;
|
||||
return this.doLogin(loginOptions);
|
||||
}
|
||||
}
|
36
lib/actions-oclif/logout.ts
Normal file
36
lib/actions-oclif/logout.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
export default class LogoutCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Logout from balena.
|
||||
|
||||
Logout from your balena account.
|
||||
`;
|
||||
public static examples = ['$ balena logout'];
|
||||
|
||||
public static usage = 'logout';
|
||||
|
||||
public async run() {
|
||||
this.parse<{}, {}>(LogoutCmd);
|
||||
await getBalenaSdk().auth.logout();
|
||||
}
|
||||
}
|
94
lib/actions-oclif/note.ts
Normal file
94
lib/actions-oclif/note.ts
Normal file
@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
device?: string; // device UUID
|
||||
dev?: string; // Alias for device.
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
note: string;
|
||||
}
|
||||
|
||||
export default class NoteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a device note.
|
||||
|
||||
Set or update a device note. If the note argument is not provided,
|
||||
it will be read from stdin.
|
||||
|
||||
To view device notes, use the \`balena device <uuid>\` command.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena note "My useful note" --device 7cf02a6',
|
||||
'$ cat note.txt | balena note --device 7cf02a6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'note',
|
||||
description: 'note content',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'note <|note>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
device: { exclusive: ['dev'], ...cf.device },
|
||||
dev: flags.string({
|
||||
exclusive: ['device'],
|
||||
hidden: true,
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static readStdin = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
NoteCmd,
|
||||
);
|
||||
|
||||
params.note = params.note || this.stdin;
|
||||
|
||||
if (_.isEmpty(params.note)) {
|
||||
throw new ExpectedError('Missing note content');
|
||||
}
|
||||
|
||||
options.device = options.device || options.dev;
|
||||
delete options.dev;
|
||||
|
||||
if (_.isEmpty(options.device)) {
|
||||
throw new ExpectedError('Missing device UUID (--device)');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
return balena.models.device.note(options.device!, params.note);
|
||||
}
|
||||
}
|
497
lib/actions-oclif/os/configure.ts
Normal file
497
lib/actions-oclif/os/configure.ts
Normal file
@ -0,0 +1,497 @@
|
||||
/**
|
||||
* @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 { flags } from '@oclif/command';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import Bluebird = require('bluebird');
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import Command from '../../command';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
const BOOT_PARTITION = 1;
|
||||
const CONNECTIONS_FOLDER = '/system-connections';
|
||||
|
||||
interface FlagsDef {
|
||||
advanced?: boolean;
|
||||
app?: string;
|
||||
application?: string;
|
||||
config?: string;
|
||||
'config-app-update-poll-interval'?: number;
|
||||
'config-network'?: string;
|
||||
'config-wifi-key'?: string;
|
||||
'config-wifi-ssid'?: string;
|
||||
device?: string; // device UUID
|
||||
'device-api-key'?: string;
|
||||
'device-type'?: string;
|
||||
help?: void;
|
||||
version?: string;
|
||||
'system-connection': string[];
|
||||
'initial-device-name'?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface DeferredDevice extends BalenaSdk.Device {
|
||||
belongs_to__application: BalenaSdk.PineDeferred;
|
||||
}
|
||||
|
||||
interface Answers {
|
||||
appUpdatePollInterval: number; // in minutes
|
||||
deviceType: string; // e.g. "raspberrypi3"
|
||||
network: 'ethernet' | 'wifi';
|
||||
version: string; // e.g. "2.32.0+rev1"
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
}
|
||||
|
||||
const deviceApiKeyDeprecationMsg = stripIndent`
|
||||
The --device-api-key option is deprecated and will be removed in a future release.
|
||||
A suitable key is automatically generated or fetched if this option is omitted.`;
|
||||
|
||||
export default class OsConfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Configure a previously downloaded balenaOS image.
|
||||
|
||||
Configure a previously downloaded balenaOS image for a specific device type or
|
||||
balena application.
|
||||
|
||||
Configuration settings such as WiFi authentication will be taken from the
|
||||
following sources, in precedence order:
|
||||
1. Command-line options like \`--config-wifi-ssid\`
|
||||
2. A given \`config.json\` file specified with the \`--config\` option.
|
||||
3. User input through interactive prompts (text menus).
|
||||
|
||||
The --device-type option may be used to override the application's default
|
||||
device type, in case of an application with mixed device types.
|
||||
|
||||
The --system-connection (-c) option can be used to inject NetworkManager connection
|
||||
profiles for additional network interfaces, such as cellular/GSM or additional
|
||||
WiFi or ethernet connections. This option may be passed multiple times in case there
|
||||
are multiple files to inject. See connection profile examples and reference at:
|
||||
https://www.balena.io/docs/reference/OS/network/2.x/
|
||||
https://developer.gnome.org/NetworkManager/stable/nm-settings.html
|
||||
|
||||
${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t')}
|
||||
|
||||
Note: This command is currently not supported on Windows natively. Windows users
|
||||
are advised to install the Windows Subsystem for Linux (WSL) with Ubuntu, and use
|
||||
the Linux release of the balena CLI:
|
||||
https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6',
|
||||
'$ balena os configure ../path/rpi3.img --device 7cf02a6 --device-api-key <existingDeviceKey>',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp',
|
||||
'$ balena os configure ../path/rpi3.img --app MyApp --version 2.12.7',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3',
|
||||
'$ balena os configure ../path/rpi3.img --app MyFinApp --device-type raspberrypi3 --config myWifiConfig.json',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'image',
|
||||
required: true,
|
||||
description: 'path to a balenaOS image file, e.g. "rpi3.img"',
|
||||
},
|
||||
];
|
||||
|
||||
// hardcoded 'os configure' to avoid oclif's 'os:configure' topic syntax
|
||||
public static usage =
|
||||
'os configure ' +
|
||||
new CommandHelp({ args: OsConfigureCmd.args }).defaultUsage();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
advanced: flags.boolean({
|
||||
char: 'v',
|
||||
description:
|
||||
'ask advanced configuration questions (when in interactive mode)',
|
||||
}),
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device'],
|
||||
}),
|
||||
application: { exclusive: ['app', 'device'], ...cf.application },
|
||||
config: flags.string({
|
||||
description:
|
||||
'path to a pre-generated config.json file to be injected in the OS image',
|
||||
}),
|
||||
'config-app-update-poll-interval': flags.integer({
|
||||
description:
|
||||
'interval (in minutes) for the on-device balena supervisor periodic app update check',
|
||||
}),
|
||||
'config-network': flags.string({
|
||||
description: 'device network type (non-interactive configuration)',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
'config-wifi-key': flags.string({
|
||||
description: 'WiFi key (password) (non-interactive configuration)',
|
||||
}),
|
||||
'config-wifi-ssid': flags.string({
|
||||
description: 'WiFi SSID (network name) (non-interactive configuration)',
|
||||
}),
|
||||
device: { exclusive: ['app', 'application'], ...cf.device },
|
||||
'device-api-key': flags.string({
|
||||
char: 'k',
|
||||
description:
|
||||
'custom device API key (DEPRECATED and only supported with balenaOS 2.0.3+)',
|
||||
}),
|
||||
'device-type': flags.string({
|
||||
description:
|
||||
'device type slug (e.g. "raspberrypi3") to override the application device type',
|
||||
}),
|
||||
'initial-device-name': flags.string({
|
||||
description:
|
||||
'This option will set the device name when the device provisions',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
'system-connection': flags.string({
|
||||
multiple: true,
|
||||
char: 'c',
|
||||
required: false,
|
||||
description:
|
||||
"paths to local files to place into the 'system-connections' directory",
|
||||
}),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
OsConfigureCmd,
|
||||
);
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
options.app = undefined;
|
||||
|
||||
await validateOptions(options);
|
||||
|
||||
const devInit = await import('balena-device-init');
|
||||
const { promises: fs } = await import('fs');
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const imagefs = await require('resin-image-fs');
|
||||
let app: BalenaSdk.Application | undefined;
|
||||
let device: BalenaSdk.Device | undefined;
|
||||
let deviceTypeSlug: string;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
if (options.device) {
|
||||
device = await balena.models['device'].get(options.device);
|
||||
deviceTypeSlug = device.device_type;
|
||||
} else {
|
||||
app = await balena.models['application'].get(options.application!);
|
||||
await checkDeviceTypeCompatibility(balena, options, app);
|
||||
deviceTypeSlug = options['device-type'] || app.device_type;
|
||||
}
|
||||
|
||||
const deviceTypeManifest = await helpers.getManifest(
|
||||
params.image,
|
||||
deviceTypeSlug,
|
||||
);
|
||||
|
||||
let configJson: import('../../utils/config').ImgConfig | undefined;
|
||||
if (options.config) {
|
||||
const rawConfig = await fs.readFile(options.config, 'utf8');
|
||||
configJson = JSON.parse(rawConfig);
|
||||
}
|
||||
|
||||
const answers: Answers = await askQuestionsForDeviceType(
|
||||
deviceTypeManifest,
|
||||
options,
|
||||
configJson,
|
||||
);
|
||||
if (options.application) {
|
||||
answers.deviceType = deviceTypeSlug;
|
||||
}
|
||||
answers.version =
|
||||
options.version ||
|
||||
(await getOsVersionFromImage(params.image, deviceTypeManifest, devInit));
|
||||
|
||||
if (_.isEmpty(configJson)) {
|
||||
if (device) {
|
||||
configJson = await generateDeviceConfig(
|
||||
device as DeferredDevice,
|
||||
options['device-api-key'],
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
configJson = await generateApplicationConfig(app!, answers);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
options['initial-device-name'] &&
|
||||
options['initial-device-name'] !== ''
|
||||
) {
|
||||
configJson!.initialDeviceName = options['initial-device-name'];
|
||||
}
|
||||
|
||||
console.info('Configuring operating system image');
|
||||
|
||||
const image = params.image;
|
||||
await helpers.osProgressHandler(
|
||||
await devInit.configure(
|
||||
image,
|
||||
deviceTypeManifest,
|
||||
configJson || {},
|
||||
answers,
|
||||
),
|
||||
);
|
||||
|
||||
if (options['system-connection']) {
|
||||
const files = await Bluebird.map(
|
||||
options['system-connection'],
|
||||
async (filePath) => {
|
||||
const content = await fs.readFile(filePath, 'utf8');
|
||||
const name = path.basename(filePath);
|
||||
|
||||
return {
|
||||
name,
|
||||
content,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await Bluebird.each(files, async ({ name, content }) => {
|
||||
await imagefs.writeFile(
|
||||
{
|
||||
image,
|
||||
partition: BOOT_PARTITION,
|
||||
path: path.join(CONNECTIONS_FOLDER, name),
|
||||
},
|
||||
content,
|
||||
);
|
||||
console.info(`Copied system-connection file: ${name}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function validateOptions(options: FlagsDef) {
|
||||
if (process.platform === 'win32') {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Unsupported platform error: the 'balena os configure' command currently requires
|
||||
the Windows Subsystem for Linux in order to run on Windows. It was tested with
|
||||
the Ubuntu 18.04 distribution from the Microsoft Store. With WSL, a balena CLI
|
||||
release for Linux (rather than Windows) should be installed: for example, the
|
||||
standalone zip package for Linux. (It is possible to have both a Windows CLI
|
||||
release and a Linux CLI release installed simultaneously.) For more information
|
||||
on WSL and the balena CLI installation options, please check:
|
||||
- https://docs.microsoft.com/en-us/windows/wsl/about
|
||||
- https://github.com/balena-io/balena-cli/blob/master/INSTALL.md
|
||||
`);
|
||||
}
|
||||
// The 'device' and 'application' options are declared "exclusive" in the oclif
|
||||
// flag definitions above, so oclif will enforce that they are not both used together.
|
||||
if (!options.device && !options.application) {
|
||||
throw new ExpectedError(
|
||||
"Either the '--device' or the '--application' option must be provided",
|
||||
);
|
||||
}
|
||||
if (!options.application && options['device-type']) {
|
||||
throw new ExpectedError(
|
||||
"The '--device-type' option can only be used in conjunction with the '--application' option",
|
||||
);
|
||||
}
|
||||
if (options['device-api-key']) {
|
||||
console.error(stripIndent`
|
||||
-------------------------------------------------------------------------------------------
|
||||
Warning: ${deviceApiKeyDeprecationMsg.split('\n').join('\n\t\t\t')}
|
||||
-------------------------------------------------------------------------------------------
|
||||
`);
|
||||
}
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around balena-device-init.getImageOsVersion(). Throws ExpectedError
|
||||
* if the OS image could not be read or the OS version could not be extracted
|
||||
* from it.
|
||||
* @param imagePath Local filesystem path to a balenaOS image file
|
||||
* @param deviceTypeManifest Device type manifest object
|
||||
*/
|
||||
async function getOsVersionFromImage(
|
||||
imagePath: string,
|
||||
deviceTypeManifest: BalenaSdk.DeviceType,
|
||||
devInit: typeof import('balena-device-init'),
|
||||
): Promise<string> {
|
||||
const osVersion = await devInit.getImageOsVersion(
|
||||
imagePath,
|
||||
deviceTypeManifest,
|
||||
);
|
||||
if (!osVersion) {
|
||||
throw new ExpectedError(stripIndent`
|
||||
Could not read OS version from the image. Please specify the balenaOS
|
||||
version manually with the --version command-line option.`);
|
||||
}
|
||||
return osVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that options['device-type'], e.g. 'raspberrypi3', is compatible with
|
||||
* app.device_type, e.g. 'raspberry-pi2'. Throws ExpectedError if they are not
|
||||
* compatible.
|
||||
* @param sdk Balena Node SDK instance
|
||||
* @param options oclif command-line options object
|
||||
* @param app Balena SDK Application model object
|
||||
*/
|
||||
async function checkDeviceTypeCompatibility(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
options: FlagsDef,
|
||||
app: BalenaSdk.Application,
|
||||
) {
|
||||
if (options['device-type']) {
|
||||
const [appDeviceType, optionDeviceType] = await Promise.all([
|
||||
sdk.models.device.getManifestBySlug(app.device_type),
|
||||
sdk.models.device.getManifestBySlug(options['device-type']),
|
||||
]);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (!helpers.areDeviceTypesCompatible(appDeviceType, optionDeviceType)) {
|
||||
throw new ExpectedError(
|
||||
`Device type ${options['device-type']} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given options or configJson objects (in this order) contain
|
||||
* the answers to some configuration questions, and interactively ask the
|
||||
* user the questions for which answers are missing. Questions such as:
|
||||
*
|
||||
* ? Network Connection (Use arrow keys)
|
||||
* ethernet
|
||||
* ❯ wifi
|
||||
* ? Network Connection wifi
|
||||
* ? Wifi SSID i-ssid
|
||||
* ? Wifi Passphrase [input is hidden]
|
||||
*
|
||||
* The questions are extracted from the given deviceType "manifest".
|
||||
*/
|
||||
async function askQuestionsForDeviceType(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
options: FlagsDef,
|
||||
configJson?: import('../../utils/config').ImgConfig,
|
||||
): Promise<Answers> {
|
||||
const form = await import('resin-cli-form');
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const answerSources: any[] = [camelifyConfigOptions(options)];
|
||||
const defaultAnswers: Partial<Answers> = {};
|
||||
const questions: any = deviceType.options;
|
||||
let extraOpts: { override: object } | undefined;
|
||||
|
||||
if (!_.isEmpty(configJson)) {
|
||||
answerSources.push(configJson);
|
||||
}
|
||||
|
||||
if (!options.advanced) {
|
||||
const advancedGroup: any = _.find(questions, {
|
||||
name: 'advanced',
|
||||
isGroup: true,
|
||||
});
|
||||
if (!_.isEmpty(advancedGroup)) {
|
||||
answerSources.push(helpers.getGroupDefaults(advancedGroup));
|
||||
}
|
||||
}
|
||||
|
||||
for (const questionName of getQuestionNames(deviceType)) {
|
||||
for (const answerSource of answerSources) {
|
||||
if (answerSource[questionName] != null) {
|
||||
defaultAnswers[questionName] = answerSource[questionName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!defaultAnswers.network &&
|
||||
(defaultAnswers.wifiSsid || defaultAnswers.wifiKey)
|
||||
) {
|
||||
defaultAnswers.network = 'wifi';
|
||||
}
|
||||
|
||||
if (!_.isEmpty(defaultAnswers)) {
|
||||
extraOpts = { override: defaultAnswers };
|
||||
}
|
||||
|
||||
return form.run(questions, extraOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a deviceType "manifest" containing "options" properties, return an
|
||||
* array of "question names" as in the following example.
|
||||
*
|
||||
* @param deviceType Device type "manifest", for example:
|
||||
* { "slug": "raspberrypi3",
|
||||
* "options": [{
|
||||
* "options": [ {
|
||||
* "name": "network",
|
||||
* "choices": ["ethernet", "wifi"],
|
||||
* ... }, {
|
||||
* "name": "wifiSsid",
|
||||
* "type": "text",
|
||||
* ... }, {
|
||||
* "options": [ {
|
||||
* "name": "appUpdatePollInterval",
|
||||
* "default": 10,
|
||||
* ...
|
||||
* @return Array of question names, for example:
|
||||
* [ 'network', 'wifiSsid', 'wifiKey', 'appUpdatePollInterval' ]
|
||||
*/
|
||||
function getQuestionNames(
|
||||
deviceType: BalenaSdk.DeviceType,
|
||||
): Array<keyof Answers> {
|
||||
const questionNames: string[] = _.chain(deviceType.options)
|
||||
.flatMap(
|
||||
(group: BalenaSdk.DeviceTypeOptions) =>
|
||||
(group.isGroup && group.options) || [],
|
||||
)
|
||||
.map((groupOption: BalenaSdk.DeviceTypeOptionsGroup) => groupOption.name)
|
||||
.filter()
|
||||
.value();
|
||||
return questionNames as Array<keyof Answers>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a new object with the key-value pairs from the input object,
|
||||
* renaming keys that start with the 'config-' prefix as follows:
|
||||
* Sample input:
|
||||
* { app: 'foo', 'config-wifi-key': 'mykey', 'config-wifi-ssid': 'myssid' }
|
||||
* Output:
|
||||
* { app: 'foo', wifiKey: 'mykey', wifiSsid: 'myssid' }
|
||||
*/
|
||||
function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
|
||||
return _.mapKeys(options, (_value, key) => {
|
||||
if (key.startsWith('config-')) {
|
||||
return key
|
||||
.substring('config-'.length)
|
||||
.replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase());
|
||||
}
|
||||
return key;
|
||||
});
|
||||
}
|
174
lib/actions-oclif/scan.ts
Normal file
174
lib/actions-oclif/scan.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { LocalBalenaOsDevice } from 'balena-sync';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getVisuals } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
verbose: boolean;
|
||||
timeout?: number;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ScanCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Scan for balenaOS devices on your local network.
|
||||
|
||||
Scan for balenaOS devices on your local network.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena scan',
|
||||
'$ balena scan --timeout 120',
|
||||
'$ balena scan --verbose',
|
||||
];
|
||||
|
||||
public static usage = 'scan';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
verbose: flags.boolean({
|
||||
char: 'v',
|
||||
default: false,
|
||||
description: 'display full info',
|
||||
}),
|
||||
timeout: flags.integer({
|
||||
char: 't',
|
||||
description: 'scan timeout in seconds',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const Bluebird = await import('bluebird');
|
||||
const _ = await import('lodash');
|
||||
const { SpinnerPromise } = getVisuals();
|
||||
const { discover } = await import('balena-sync');
|
||||
const prettyjson = await import('prettyjson');
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const { dockerPort, dockerTimeout } = await import(
|
||||
'../actions/local/common'
|
||||
);
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
|
||||
|
||||
const discoverTimeout =
|
||||
options.timeout != null ? options.timeout * 1000 : undefined;
|
||||
|
||||
// Find active local devices
|
||||
const activeLocalDevices: LocalBalenaOsDevice[] = await new SpinnerPromise({
|
||||
promise: discover.discoverLocalBalenaOsDevices(discoverTimeout),
|
||||
startMessage: 'Scanning for local balenaOS devices..',
|
||||
stopMessage: 'Reporting scan results',
|
||||
}).filter(async ({ address }: { address: string }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
try {
|
||||
await docker.pingAsync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Exit with message if no devices found
|
||||
if (_.isEmpty(activeLocalDevices)) {
|
||||
// TODO: Consider whether this should really be an error
|
||||
throw new ExpectedError(
|
||||
process.platform === 'win32'
|
||||
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
|
||||
: ScanCmd.noDevicesFoundMessage,
|
||||
);
|
||||
}
|
||||
|
||||
// Query devices for info
|
||||
const devicesInfo = await Bluebird.map(
|
||||
activeLocalDevices,
|
||||
({ host, address }) => {
|
||||
const docker = dockerUtils.createClient({
|
||||
host: address,
|
||||
port: dockerPort,
|
||||
timeout: dockerTimeout,
|
||||
}) as any;
|
||||
return Bluebird.props({
|
||||
host,
|
||||
address,
|
||||
dockerInfo: docker
|
||||
.infoAsync()
|
||||
.catchReturn('Could not get Docker info'),
|
||||
dockerVersion: docker
|
||||
.versionAsync()
|
||||
.catchReturn('Could not get Docker version'),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Reduce properties if not --verbose
|
||||
if (!options.verbose) {
|
||||
devicesInfo.forEach((d: any) => {
|
||||
d.dockerInfo = _.isObject(d.dockerInfo)
|
||||
? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties)
|
||||
: d.dockerInfo;
|
||||
d.dockerVersion = _.isObject(d.dockerVersion)
|
||||
? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties)
|
||||
: d.dockerVersion;
|
||||
});
|
||||
}
|
||||
|
||||
// Output results
|
||||
console.log(prettyjson.render(devicesInfo, { noColor: true }));
|
||||
}
|
||||
|
||||
protected static dockerInfoProperties = [
|
||||
'Containers',
|
||||
'ContainersRunning',
|
||||
'ContainersPaused',
|
||||
'ContainersStopped',
|
||||
'Images',
|
||||
'Driver',
|
||||
'SystemTime',
|
||||
'KernelVersion',
|
||||
'OperatingSystem',
|
||||
'Architecture',
|
||||
];
|
||||
|
||||
protected static dockerVersionProperties = ['Version', 'ApiVersion'];
|
||||
|
||||
protected static noDevicesFoundMessage =
|
||||
'Could not find any balenaOS devices on the local network.';
|
||||
|
||||
protected static windowsTipMessage = `
|
||||
|
||||
Note for Windows users:
|
||||
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
|
||||
installed (Control Panel > Programs and Features). If not, you can download
|
||||
Bonjour for Windows (included with Bonjour Print Services) from here:
|
||||
https://support.apple.com/kb/DL999
|
||||
|
||||
After installing Bonjour, restart your PC and run the 'balena scan' command
|
||||
again.`;
|
||||
}
|
52
lib/actions-oclif/settings.ts
Normal file
52
lib/actions-oclif/settings.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class SettingsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Print current settings.
|
||||
|
||||
Use this command to display current balena CLI settings.
|
||||
`;
|
||||
public static examples = ['$ balena settings'];
|
||||
|
||||
public static usage = 'settings';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
this.parse<FlagsDef, {}>(SettingsCmd);
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
|
||||
return getBalenaSdk()
|
||||
.settings.getAll()
|
||||
.then(prettyjson.render)
|
||||
.then(console.log);
|
||||
}
|
||||
}
|
134
lib/actions-oclif/tag/rm.ts
Normal file
134
lib/actions-oclif/tag/rm.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../../utils/normalization';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
tagKey: string;
|
||||
}
|
||||
|
||||
export default class TagRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove a tag from an application, device or release.
|
||||
|
||||
Remove a tag from an application, device or release.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag rm myTagKey --application MyApp',
|
||||
'$ balena tag rm myTagKey --device 7cf02a6',
|
||||
'$ balena tag rm myTagKey --release 1234',
|
||||
'$ balena tag rm myTagKey --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'tagKey',
|
||||
description: 'the key string of the tag',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'tag rm <tagKey>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
TagRmCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(TagRmCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.remove(
|
||||
tryAsInteger(options.application),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.remove(
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.remove(releaseParam, params.tagKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To remove a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tag rm
|
||||
`;
|
||||
}
|
157
lib/actions-oclif/tag/set.ts
Normal file
157
lib/actions-oclif/tag/set.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../../utils/normalization';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
tagKey: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export default class TagSetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Set a tag on an application, device or release.
|
||||
|
||||
Set a tag on an application, device or release.
|
||||
|
||||
You can optionally provide a value to be associated with the created
|
||||
tag, as an extra argument after the tag key. If a value isn't
|
||||
provided, a tag with an empty value is created.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tag set mySimpleTag --application MyApp',
|
||||
'$ balena tag set myCompositeTag myTagValue --application MyApp',
|
||||
'$ balena tag set myCompositeTag myTagValue --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag "my tag value with whitespaces" --device 7cf02a6',
|
||||
'$ balena tag set myCompositeTag myTagValue --release 1234',
|
||||
'$ balena tag set myCompositeTag --release 1234',
|
||||
'$ balena tag set myCompositeTag --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'tagKey',
|
||||
description: 'the key string of the tag',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
description: 'the optional value associated with the tag',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'tag set <tagKey> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
TagSetCmd,
|
||||
);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(TagSetCmd.missingResourceMessage);
|
||||
}
|
||||
|
||||
if (params.value == null) {
|
||||
params.value = '';
|
||||
}
|
||||
|
||||
if (options.application) {
|
||||
return balena.models.application.tags.set(
|
||||
tryAsInteger(options.application),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
return balena.models.device.tags.set(
|
||||
tryAsInteger(options.device),
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
return balena.models.release.tags.set(
|
||||
releaseParam,
|
||||
params.tagKey,
|
||||
params.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected static missingResourceMessage = stripIndent`
|
||||
To set a resource tag, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tag set
|
||||
`;
|
||||
}
|
132
lib/actions-oclif/tags.ts
Normal file
132
lib/actions-oclif/tags.ts
Normal file
@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
import { disambiguateReleaseParam } from '../utils/normalization';
|
||||
import { tryAsInteger } from '../utils/validation';
|
||||
import { isV12 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
device?: string;
|
||||
release?: string;
|
||||
help: void;
|
||||
app?: string;
|
||||
}
|
||||
|
||||
export default class TagsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all tags for an application, device or release.
|
||||
|
||||
List all tags and their values for a particular application,
|
||||
device or release.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena tags --application MyApp',
|
||||
'$ balena tags --device 7cf02a6',
|
||||
'$ balena tags --release 1234',
|
||||
'$ balena tags --release b376b0e544e9429483b656490e5b9443b4349bd6',
|
||||
];
|
||||
|
||||
public static usage = 'tags';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: {
|
||||
...cf.application,
|
||||
exclusive: ['app', 'device', 'release'],
|
||||
},
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: ['app', 'application', 'release'],
|
||||
},
|
||||
release: {
|
||||
...cf.release,
|
||||
exclusive: ['app', 'application', 'device'],
|
||||
},
|
||||
help: cf.help,
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device', 'release'],
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(TagsCmd);
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Check user has specified one of application/device/release
|
||||
if (!options.application && !options.device && !options.release) {
|
||||
throw new ExpectedError(this.missingResourceMessage);
|
||||
}
|
||||
|
||||
let tags;
|
||||
|
||||
if (options.application) {
|
||||
tags = await balena.models.application.tags.getAllByApplication(
|
||||
tryAsInteger(options.application),
|
||||
);
|
||||
}
|
||||
if (options.device) {
|
||||
tags = await balena.models.device.tags.getAllByDevice(
|
||||
tryAsInteger(options.device),
|
||||
);
|
||||
}
|
||||
if (options.release) {
|
||||
const releaseParam = await disambiguateReleaseParam(
|
||||
balena,
|
||||
options.release,
|
||||
);
|
||||
|
||||
tags = await balena.models.release.tags.getAllByRelease(releaseParam);
|
||||
}
|
||||
|
||||
if (!tags || tags.length === 0) {
|
||||
throw new ExpectedError('No tags found');
|
||||
}
|
||||
|
||||
console.log(
|
||||
isV12()
|
||||
? getVisuals().table.horizontal(tags, ['tag_key', 'value'])
|
||||
: getVisuals().table.horizontal(tags, ['id', 'tag_key', 'value']),
|
||||
);
|
||||
}
|
||||
|
||||
protected missingResourceMessage = stripIndent`
|
||||
To list tags for a resource, you must provide exactly one of:
|
||||
|
||||
* An application, with --application <appname>
|
||||
* A device, with --device <uuid>
|
||||
* A release, with --release <id or commit>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help tags
|
||||
`;
|
||||
}
|
89
lib/actions-oclif/version.ts
Normal file
89
lib/actions-oclif/version.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 { flags } from '@oclif/command';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
|
||||
interface FlagsDef {
|
||||
all?: boolean;
|
||||
json?: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export interface JsonVersions {
|
||||
'balena-cli': string;
|
||||
'Node.js': string;
|
||||
}
|
||||
|
||||
export default class VersionCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
|
||||
Display version information for the balena CLI and/or Node.js.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because the JSON format is less likely to change and it better represents
|
||||
data types like lists and empty strings. The 'jq' utility may be helpful
|
||||
in shell scripts (https://stedolan.github.io/jq/manual/).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena version',
|
||||
'$ balena version -a',
|
||||
'$ balena version -j',
|
||||
];
|
||||
|
||||
public static usage = 'version';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
all: flags.boolean({
|
||||
char: 'a',
|
||||
default: false,
|
||||
description:
|
||||
'include version information for additional components (Node.js)',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
default: false,
|
||||
description:
|
||||
'output version information in JSON format for programmatic use',
|
||||
}),
|
||||
help: flags.help({ char: 'h' }),
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(VersionCmd);
|
||||
const versions: JsonVersions = {
|
||||
'balena-cli': (await import('../../package.json')).version,
|
||||
'Node.js':
|
||||
process.version && process.version.startsWith('v')
|
||||
? process.version.slice(1)
|
||||
: process.version,
|
||||
};
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(versions, null, 4));
|
||||
} else {
|
||||
if (options.all) {
|
||||
console.log(`balena-cli version "${versions['balena-cli']}"`);
|
||||
console.log(`Node.js version "${versions['Node.js']}"`);
|
||||
} else {
|
||||
// backwards compatibility
|
||||
console.log(versions['balena-cli']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
lib/actions-oclif/whoami.ts
Normal file
54
lib/actions-oclif/whoami.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { stripIndent } from 'common-tags';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
export default class WhoamiCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get current username and email address.
|
||||
|
||||
Get the username and email address of the currently logged in user.
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena whoami'];
|
||||
|
||||
public static usage = 'whoami';
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
this.parse<{}, {}>(WhoamiCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const [username, email, url] = await Promise.all([
|
||||
balena.auth.whoami(),
|
||||
balena.auth.getEmail(),
|
||||
balena.settings.get('balenaUrl'),
|
||||
]);
|
||||
console.log(
|
||||
getVisuals().table.vertical({ username, email, url }, [
|
||||
'$account information$',
|
||||
'username',
|
||||
'email',
|
||||
'url',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,157 +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.create =
|
||||
signature: 'app create <name>'
|
||||
description: 'create an application'
|
||||
help: '''
|
||||
Use this command to create a new resin.io 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
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app create MyApp
|
||||
$ resin app create MyApp --type raspberry-pi
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
parameter: 'type'
|
||||
description: 'application device type (Check available types with `resin devices supported`)'
|
||||
alias: 't'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
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) ->
|
||||
if hasApplication
|
||||
throw new Error('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)
|
||||
.then (application) ->
|
||||
console.info("Application created: #{application.app_name} (#{application.device_type}, id #{application.id})")
|
||||
.nodeify(done)
|
||||
|
||||
exports.list =
|
||||
signature: 'apps'
|
||||
description: 'list all applications'
|
||||
help: '''
|
||||
Use this command to list all your applications.
|
||||
|
||||
Notice this command only shows the most important bits of information for each app.
|
||||
If you want detailed information, use resin app <name> instead.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin apps
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.getAll().then (applications) ->
|
||||
console.log visuals.table.horizontal applications, [
|
||||
'id'
|
||||
'app_name'
|
||||
'device_type'
|
||||
'online_devices'
|
||||
'devices_length'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'app <name>'
|
||||
description: 'list a single application'
|
||||
help: '''
|
||||
Use this command to show detailed information for a single application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.application.get(params.name).then (application) ->
|
||||
console.log visuals.table.vertical application, [
|
||||
"$#{application.app_name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'git_repository'
|
||||
'commit'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.restart =
|
||||
signature: 'app restart <name>'
|
||||
description: 'restart an application'
|
||||
help: '''
|
||||
Use this command to restart all devices that belongs to a certain application.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin app restart MyApp
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.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.
|
||||
|
||||
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
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
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 application?').then ->
|
||||
resin.models.application.remove(params.name)
|
||||
.nodeify(done)
|
@ -1,206 +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.login =
|
||||
signature: 'login'
|
||||
description: 'login to resin.io'
|
||||
help: '''
|
||||
Use this command to login to your resin.io account.
|
||||
|
||||
This command will prompt you to login using the following login types:
|
||||
|
||||
- Web authorization: open your web browser and prompt you to authorize the CLI
|
||||
from the dashboard.
|
||||
|
||||
- Credentials: using email/password and 2FA.
|
||||
|
||||
- Token: using the authentication token 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
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'token'
|
||||
description: 'auth token'
|
||||
parameter: 'token'
|
||||
alias: 't'
|
||||
}
|
||||
{
|
||||
signature: 'web'
|
||||
description: 'web-based login'
|
||||
boolean: true
|
||||
alias: 'w'
|
||||
}
|
||||
{
|
||||
signature: 'credentials'
|
||||
description: 'credential-based login'
|
||||
boolean: true
|
||||
alias: 'c'
|
||||
}
|
||||
{
|
||||
signature: 'email'
|
||||
parameter: 'email'
|
||||
description: 'email'
|
||||
alias: [ 'e', 'u' ]
|
||||
}
|
||||
{
|
||||
signature: 'password'
|
||||
parameter: 'password'
|
||||
description: 'password'
|
||||
alias: 'p'
|
||||
}
|
||||
]
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
auth = require('../auth')
|
||||
form = require('resin-cli-form')
|
||||
patterns = require('../utils/patterns')
|
||||
messages = require('../utils/messages')
|
||||
|
||||
login = (options) ->
|
||||
if options.token?
|
||||
return Promise.try ->
|
||||
return options.token if _.isString(options.token)
|
||||
return form.ask
|
||||
message: 'Token (from the preferences page)'
|
||||
name: 'token'
|
||||
type: 'input'
|
||||
.then(resin.auth.loginWithToken)
|
||||
else if options.credentials
|
||||
return patterns.authenticate(options)
|
||||
else if options.web
|
||||
console.info('Connecting to the web dashboard')
|
||||
return auth.login()
|
||||
|
||||
return patterns.askLoginType().then (loginType) ->
|
||||
|
||||
if loginType is 'register'
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
return capitanoRunAsync('signup')
|
||||
|
||||
options[loginType] = true
|
||||
return login(options)
|
||||
|
||||
resin.settings.get('resinUrl').then (resinUrl) ->
|
||||
console.log(messages.resinAsciiArt)
|
||||
console.log("\nLogging in to #{resinUrl}")
|
||||
return login(options)
|
||||
.then(resin.auth.whoami)
|
||||
.tap (username) ->
|
||||
console.info("Successfully logged in as: #{username}")
|
||||
console.info """
|
||||
|
||||
Find out about the available commands by running:
|
||||
|
||||
$ resin help
|
||||
|
||||
#{messages.reachingOut}
|
||||
"""
|
||||
.nodeify(done)
|
||||
|
||||
exports.logout =
|
||||
signature: 'logout'
|
||||
description: 'logout from resin.io'
|
||||
help: '''
|
||||
Use this command to logout from your resin.io account.o
|
||||
|
||||
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
|
||||
'''
|
||||
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)
|
||||
|
||||
exports.whoami =
|
||||
signature: 'whoami'
|
||||
description: 'get current username and email address'
|
||||
help: '''
|
||||
Use this command to find out the current logged in username and email address.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin whoami
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.props
|
||||
username: resin.auth.whoami()
|
||||
email: resin.auth.getEmail()
|
||||
url: resin.settings.get('resinUrl')
|
||||
.then (results) ->
|
||||
console.log visuals.table.vertical results, [
|
||||
'$account information$'
|
||||
'username'
|
||||
'email'
|
||||
'url'
|
||||
]
|
||||
.nodeify(done)
|
@ -1,64 +0,0 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
getBundleInfo = Promise.method (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
You must provide either an application or a device-type/architecture
|
||||
pair to use the resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
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 [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
description: 'The architecture to build for'
|
||||
alias: 'A'
|
||||
},
|
||||
{
|
||||
signature: 'deviceType'
|
||||
parameter: 'deviceType'
|
||||
description: 'The type of device this build is for'
|
||||
alias: 'd'
|
||||
},
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target resin.io application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Logger = require('../utils/logger')
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, new Logger())
|
||||
.asCallback(done)
|
||||
|
207
lib/actions/build.js
Normal file
207
lib/actions/build.js
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Imported here because it's needed for the setup
|
||||
// of this action
|
||||
import * as Bluebird from 'bluebird';
|
||||
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
import * as compose from '../utils/compose';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
/*
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the app this build is for (optional)
|
||||
arch: the architecture to build for
|
||||
deviceType: the device type to build for
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
*/
|
||||
const buildProject = function (docker, logger, composeOpts, opts) {
|
||||
const { loadProject } = require('../utils/compose_ts');
|
||||
return Bluebird.resolve(loadProject(logger, composeOpts))
|
||||
.then(function (project) {
|
||||
const appType = opts.app?.application_type?.[0];
|
||||
if (
|
||||
appType != null &&
|
||||
project.descriptors.length > 1 &&
|
||||
!appType.supports_multicontainer
|
||||
) {
|
||||
logger.logWarn(
|
||||
'Target application does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.',
|
||||
);
|
||||
}
|
||||
|
||||
return compose.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
project.composition,
|
||||
opts.arch,
|
||||
opts.deviceType,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
);
|
||||
})
|
||||
.then(function () {
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Build succeeded!');
|
||||
})
|
||||
.tapCatch(() => {
|
||||
logger.logError('Build failed');
|
||||
});
|
||||
};
|
||||
|
||||
export const build = {
|
||||
signature: 'build [source]',
|
||||
description: 'Build a single image or a multicontainer project locally',
|
||||
primary: true,
|
||||
help: `\
|
||||
Use this command to build an image or a complete multicontainer project with
|
||||
the provided docker daemon in your development machine or balena device.
|
||||
(See also the \`balena push\` command for the option of building images in the
|
||||
balenaCloud build servers.)
|
||||
|
||||
You must provide either an application or a device-type/architecture pair to use
|
||||
the balena Dockerfile pre-processor (e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file, and if found,
|
||||
each service defined in the compose file will be built. If a compose file isn't
|
||||
found, it will look for a Dockerfile[.template] file (or alternative Dockerfile
|
||||
specified with the \`--dockerfile\` option), and if no dockerfile is found, it
|
||||
will try to generate one.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena build
|
||||
$ balena build ./source/
|
||||
$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated
|
||||
$ balena build --application MyApp ./source/
|
||||
$ balena build --docker /var/run/docker.sock # Linux, Mac
|
||||
$ balena build --docker //./pipe/docker_engine # Windows
|
||||
$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem\
|
||||
`,
|
||||
options: dockerUtils.appendOptions(
|
||||
compose.appendOptions([
|
||||
{
|
||||
signature: 'arch',
|
||||
parameter: 'arch',
|
||||
description: 'The architecture to build for',
|
||||
alias: 'A',
|
||||
},
|
||||
{
|
||||
signature: 'deviceType',
|
||||
parameter: 'deviceType',
|
||||
description: 'The type of device this build is for',
|
||||
alias: 'd',
|
||||
},
|
||||
{
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'The target balena application this build is for',
|
||||
alias: 'a',
|
||||
},
|
||||
]),
|
||||
),
|
||||
action(params, options) {
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
require('events').defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
const { ExpectedError } = require('../errors');
|
||||
const { checkLoggedIn } = require('../utils/patterns');
|
||||
const { validateProjectDirectory } = require('../utils/compose_ts');
|
||||
const helpers = require('../utils/helpers');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// `build` accepts `[source]` as a parameter, but compose expects it
|
||||
// as an option. swap them here
|
||||
if (options.source == null) {
|
||||
options.source = params.source;
|
||||
}
|
||||
delete params.source;
|
||||
|
||||
const { application, arch, deviceType } = options;
|
||||
|
||||
return Bluebird.try(function () {
|
||||
if (
|
||||
(application == null && (arch == null || deviceType == null)) ||
|
||||
(application != null && (arch != null || deviceType != null))
|
||||
) {
|
||||
throw new ExpectedError(
|
||||
'You must specify either an application or an arch/deviceType pair to build for',
|
||||
);
|
||||
}
|
||||
if (application) {
|
||||
return checkLoggedIn();
|
||||
}
|
||||
})
|
||||
.then(() =>
|
||||
validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
}),
|
||||
)
|
||||
.then(function ({ dockerfilePath, registrySecrets }) {
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
|
||||
if (arch != null && deviceType != null) {
|
||||
return [undefined, arch, deviceType];
|
||||
} else {
|
||||
return helpers
|
||||
.getAppWithArch(application)
|
||||
.then((app) => [app, app.arch, app.device_type]);
|
||||
}
|
||||
})
|
||||
|
||||
.then(function ([app, resolvedArch, resolvedDeviceType]) {
|
||||
return Bluebird.join(
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
(docker, buildOpts, composeOpts) =>
|
||||
buildProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
arch: resolvedArch,
|
||||
deviceType: resolvedDeviceType,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
@ -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'
|
153
lib/actions/command-options.ts
Normal file
153
lib/actions/command-options.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
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 = {
|
||||
...optionalApplication,
|
||||
required: 'You have to specify an application',
|
||||
};
|
||||
|
||||
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 = {
|
||||
...exports.optionalOsVersion,
|
||||
required: 'You have to specify an exact os version',
|
||||
};
|
||||
|
||||
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,311 +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.read =
|
||||
signature: 'config read'
|
||||
description: 'read a device configuration'
|
||||
help: '''
|
||||
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config read --type raspberry-pi
|
||||
$ resin config read --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
prettyjson = require('prettyjson')
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
return config.read(drive, options.type)
|
||||
.tap (configJSON) ->
|
||||
console.info(prettyjson.render(configJSON))
|
||||
.nodeify(done)
|
||||
|
||||
exports.write =
|
||||
signature: 'config write <key> <value>'
|
||||
description: 'write a device configuration'
|
||||
help: '''
|
||||
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ 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 "..."
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
config = require('resin-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).then (configJSON) ->
|
||||
console.info("Setting #{params.key} to #{params.value}")
|
||||
_.set(configJSON, params.key, params.value)
|
||||
return configJSON
|
||||
.tap ->
|
||||
return umountAsync(drive)
|
||||
.then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.inject =
|
||||
signature: 'config inject <file>'
|
||||
description: 'inject a device configuration file'
|
||||
help: '''
|
||||
Use this command to inject a config.json file to the mounted filesystem (e.g. SD card) 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
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
readFileAsync = Promise.promisify(require('fs').readFile)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
readFileAsync(params.file, 'utf8').then(JSON.parse).then (configJSON) ->
|
||||
return config.write(drive, options.type, configJSON)
|
||||
.tap ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.reconfigure =
|
||||
signature: 'config reconfigure'
|
||||
description: 'reconfigure a provisioned device'
|
||||
help: '''
|
||||
Use this command to reconfigure a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config reconfigure --type raspberry-pi
|
||||
$ resin config reconfigure --type raspberry-pi --advanced
|
||||
$ resin config reconfigure --type raspberry-pi --drive /dev/disk2
|
||||
'''
|
||||
options: [
|
||||
{
|
||||
signature: 'type'
|
||||
description: 'device type (Check available types with `resin devices supported`)'
|
||||
parameter: 'type'
|
||||
alias: 't'
|
||||
required: 'You have to specify a device type'
|
||||
}
|
||||
{
|
||||
signature: 'drive'
|
||||
description: 'drive'
|
||||
parameter: 'drive'
|
||||
alias: 'd'
|
||||
}
|
||||
{
|
||||
signature: 'advanced'
|
||||
description: 'show advanced commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
config = require('resin-config-json')
|
||||
visuals = require('resin-cli-visuals')
|
||||
capitanoRunAsync = Promise.promisify(require('capitano').run)
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
|
||||
Promise.try ->
|
||||
return options.drive or visuals.drive('Select the device drive')
|
||||
.tap(umountAsync)
|
||||
.then (drive) ->
|
||||
config.read(drive, options.type).get('uuid')
|
||||
.tap ->
|
||||
umountAsync(drive)
|
||||
.then (uuid) ->
|
||||
configureCommand = "os configure #{drive} #{uuid}"
|
||||
if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
return capitanoRunAsync(configureCommand)
|
||||
.then ->
|
||||
console.info('Done')
|
||||
.nodeify(done)
|
||||
|
||||
exports.generate =
|
||||
signature: 'config generate'
|
||||
description: 'generate a config.json file'
|
||||
help: '''
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin config generate --device 7cf02a6
|
||||
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
|
||||
$ resin config generate --device 7cf02a6 --output config.json
|
||||
$ resin config generate --app MyApp
|
||||
$ resin config generate --app MyApp --output config.json
|
||||
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
commandOptions.optionalDeviceApiKey
|
||||
{
|
||||
signature: 'output'
|
||||
description: 'output'
|
||||
parameter: 'output'
|
||||
alias: 'o'
|
||||
}
|
||||
# Options for non-interactive configuration
|
||||
{
|
||||
signature: 'network'
|
||||
description: 'the network type to use: ethernet or wifi'
|
||||
parameter: 'network'
|
||||
}
|
||||
{
|
||||
signature: 'wifiSsid'
|
||||
description: 'the wifi ssid to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiSsid'
|
||||
}
|
||||
{
|
||||
signature: 'wifiKey'
|
||||
description: 'the wifi key to use (used only if --network is set to wifi)'
|
||||
parameter: 'wifiKey'
|
||||
}
|
||||
{
|
||||
signature: 'appUpdatePollInterval'
|
||||
description: 'how frequently (in minutes) to poll for application updates'
|
||||
parameter: 'appUpdatePollInterval'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
writeFileAsync = Promise.promisify(require('fs').writeFile)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
form = require('resin-cli-form')
|
||||
deviceConfig = require('resin-device-config')
|
||||
prettyjson = require('prettyjson')
|
||||
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
|
||||
|
||||
if not options.device? and not options.application?
|
||||
throw new Error '''
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ resin help config generate
|
||||
'''
|
||||
|
||||
Promise.try ->
|
||||
if options.device?
|
||||
return resin.models.device.get(options.device)
|
||||
return resin.models.application.get(options.application)
|
||||
.then (resource) ->
|
||||
resin.models.device.getManifestBySlug(resource.device_type)
|
||||
.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) ->
|
||||
if resource.uuid?
|
||||
generateDeviceConfig(resource, options.deviceApiKey, answers)
|
||||
else
|
||||
generateApplicationConfig(resource, answers)
|
||||
.then (config) ->
|
||||
deviceConfig.validate(config)
|
||||
if options.output?
|
||||
return writeFileAsync(options.output, JSON.stringify(config))
|
||||
|
||||
console.log(prettyjson.render(config))
|
||||
.nodeify(done)
|
418
lib/actions/config.js
Normal file
418
lib/actions/config.js
Normal file
@ -0,0 +1,418 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
import { normalizeUuidProp } from '../utils/normalization';
|
||||
import { getBalenaSdk, getVisuals } from '../utils/lazy';
|
||||
|
||||
export const read = {
|
||||
signature: 'config read',
|
||||
description: 'read a device configuration',
|
||||
help: `\
|
||||
Use this command to read the config.json file from the mounted filesystem (e.g. SD card) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config read --type raspberry-pi
|
||||
$ balena config read --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(_params, options) {
|
||||
const Bluebird = require('bluebird');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Bluebird.promisify(require('umount').umount);
|
||||
const prettyjson = require('prettyjson');
|
||||
|
||||
return Bluebird.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) => config.read(drive, options.type))
|
||||
.tap((configJSON) => {
|
||||
console.info(prettyjson.render(configJSON));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const write = {
|
||||
signature: 'config write <key> <value>',
|
||||
description: 'write a device configuration',
|
||||
help: `\
|
||||
Use this command to write the config.json file to the mounted filesystem (e.g. SD card) of a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config write --type raspberry-pi username johndoe
|
||||
$ balena config write --type raspberry-pi --drive /dev/disk2 username johndoe
|
||||
$ balena config write --type raspberry-pi files.network/settings "..."\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(params, options) {
|
||||
const Bluebird = require('bluebird');
|
||||
const _ = require('lodash');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Bluebird.promisify(require('umount').umount);
|
||||
|
||||
return Bluebird.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
config
|
||||
.read(drive, options.type)
|
||||
.then(function (configJSON) {
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
_.set(configJSON, params.key, params.value);
|
||||
return configJSON;
|
||||
})
|
||||
.tap(() => umountAsync(drive))
|
||||
.then((configJSON) => config.write(drive, options.type, configJSON)),
|
||||
)
|
||||
.tap(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const inject = {
|
||||
signature: 'config inject <file>',
|
||||
description: 'inject a device configuration file',
|
||||
help: `\
|
||||
Use this command to inject a config.json file to the mounted filesystem
|
||||
(e.g. SD card or mounted balenaOS image) of a provisioned device"
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config inject my/config.json --type raspberry-pi
|
||||
$ balena config inject my/config.json --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(params, options) {
|
||||
const Bluebird = require('bluebird');
|
||||
const config = require('balena-config-json');
|
||||
const umountAsync = Bluebird.promisify(require('umount').umount);
|
||||
|
||||
return Bluebird.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
require('fs')
|
||||
.promises.readFile(params.file, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.then((configJSON) => config.write(drive, options.type, configJSON)),
|
||||
)
|
||||
.tap(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const reconfigure = {
|
||||
signature: 'config reconfigure',
|
||||
description: 'reconfigure a provisioned device',
|
||||
help: `\
|
||||
Use this command to reconfigure a provisioned device
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config reconfigure --type raspberry-pi
|
||||
$ balena config reconfigure --type raspberry-pi --advanced
|
||||
$ balena config reconfigure --type raspberry-pi --drive /dev/disk2\
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'type',
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
parameter: 'type',
|
||||
alias: 't',
|
||||
required: 'You have to specify a device type',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
description: 'drive',
|
||||
parameter: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
{
|
||||
signature: 'advanced',
|
||||
description: 'show advanced commands',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
root: true,
|
||||
action(_params, options) {
|
||||
const Bluebird = require('bluebird');
|
||||
const config = require('balena-config-json');
|
||||
const { runCommand } = require('../utils/helpers');
|
||||
const umountAsync = Bluebird.promisify(require('umount').umount);
|
||||
|
||||
return Bluebird.try(
|
||||
() => options.drive || getVisuals().drive('Select the device drive'),
|
||||
)
|
||||
.tap(umountAsync)
|
||||
.then((drive) =>
|
||||
config
|
||||
.read(drive, options.type)
|
||||
.get('uuid')
|
||||
.tap(() => umountAsync(drive))
|
||||
.then(function (uuid) {
|
||||
let configureCommand = `os configure ${drive} --device ${uuid}`;
|
||||
if (options.advanced) {
|
||||
configureCommand += ' --advanced';
|
||||
}
|
||||
return runCommand(configureCommand);
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
console.info('Done');
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const generate = {
|
||||
signature: 'config generate',
|
||||
description: 'generate a config.json file',
|
||||
help: `\
|
||||
Use this command to generate a config.json for a device or application.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
|
||||
This is interactive by default, but you can do this automatically without interactivity
|
||||
by specifying an option for each question on the command line, if you know the questions
|
||||
that will be asked for the relevant device type.
|
||||
|
||||
In case that you want to configure an image for an application with mixed device types,
|
||||
you can pass the --device-type argument along with --app to specify the target device type.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
|
||||
$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7
|
||||
$ balena config generate --app MyApp --version 2.12.7 --device-type fincm3
|
||||
$ balena config generate --app MyApp --version 2.12.7 --output config.json
|
||||
$ balena config generate --app MyApp --version 2.12.7 \
|
||||
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1\
|
||||
`,
|
||||
options: [
|
||||
commandOptions.osVersion,
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.optionalDevice,
|
||||
commandOptions.optionalDeviceApiKey,
|
||||
commandOptions.optionalDeviceType,
|
||||
{
|
||||
signature: 'generate-device-api-key',
|
||||
description: 'generate a fresh device key for the device',
|
||||
boolean: true,
|
||||
},
|
||||
{
|
||||
signature: 'output',
|
||||
description: 'output',
|
||||
parameter: 'output',
|
||||
alias: 'o',
|
||||
},
|
||||
// Options for non-interactive configuration
|
||||
{
|
||||
signature: 'network',
|
||||
description: 'the network type to use: ethernet or wifi',
|
||||
parameter: 'network',
|
||||
},
|
||||
{
|
||||
signature: 'wifiSsid',
|
||||
description:
|
||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||
parameter: 'wifiSsid',
|
||||
},
|
||||
{
|
||||
signature: 'wifiKey',
|
||||
description:
|
||||
'the wifi key to use (used only if --network is set to wifi)',
|
||||
parameter: 'wifiKey',
|
||||
},
|
||||
{
|
||||
signature: 'appUpdatePollInterval',
|
||||
description:
|
||||
'how frequently (in minutes) to poll for application updates',
|
||||
parameter: 'appUpdatePollInterval',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
action(_params, options) {
|
||||
normalizeUuidProp(options, 'device');
|
||||
const Bluebird = require('bluebird');
|
||||
const balena = getBalenaSdk();
|
||||
const form = require('resin-cli-form');
|
||||
const prettyjson = require('prettyjson');
|
||||
|
||||
const {
|
||||
generateDeviceConfig,
|
||||
generateApplicationConfig,
|
||||
} = require('../utils/config');
|
||||
const helpers = require('../utils/helpers');
|
||||
const { exitWithExpectedError } = require('../errors');
|
||||
|
||||
if (options.device == null && options.application == null) {
|
||||
exitWithExpectedError(`\
|
||||
You have to pass either a device or an application.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate\
|
||||
`);
|
||||
}
|
||||
|
||||
if (!options.application && options.deviceType) {
|
||||
exitWithExpectedError(`\
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate\
|
||||
`);
|
||||
}
|
||||
|
||||
return Bluebird.try(
|
||||
/** @returns {Promise<any>} */ function () {
|
||||
if (options.device != null) {
|
||||
return balena.models.device.get(options.device);
|
||||
}
|
||||
return balena.models.application.get(options.application);
|
||||
},
|
||||
)
|
||||
.then(function (resource) {
|
||||
const deviceType = options.deviceType || resource.device_type;
|
||||
let manifestPromise = balena.models.device.getManifestBySlug(
|
||||
deviceType,
|
||||
);
|
||||
|
||||
if (options.application && options.deviceType) {
|
||||
const app = resource;
|
||||
const appManifestPromise = balena.models.device.getManifestBySlug(
|
||||
app.device_type,
|
||||
);
|
||||
manifestPromise = manifestPromise.tap((paramDeviceType) =>
|
||||
appManifestPromise.then(function (appDeviceType) {
|
||||
if (
|
||||
!helpers.areDeviceTypesCompatible(
|
||||
appDeviceType,
|
||||
paramDeviceType,
|
||||
)
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return manifestPromise
|
||||
.get('options')
|
||||
.then((
|
||||
formOptions, // Pass params as an override: if there is any param with exactly the same name as a
|
||||
) =>
|
||||
// required option, that value is used (and the corresponding question is not asked)
|
||||
form.run(formOptions, { override: options }),
|
||||
)
|
||||
.then(function (answers) {
|
||||
answers.version = options.version;
|
||||
|
||||
if (resource.uuid != null) {
|
||||
return generateDeviceConfig(
|
||||
resource,
|
||||
options.deviceApiKey || options['generate-device-api-key'],
|
||||
answers,
|
||||
);
|
||||
} else {
|
||||
answers.deviceType = deviceType;
|
||||
return generateApplicationConfig(resource, answers);
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(function (config) {
|
||||
if (options.output != null) {
|
||||
return require('fs').promises.writeFile(
|
||||
options.output,
|
||||
JSON.stringify(config),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(prettyjson.render(config));
|
||||
});
|
||||
},
|
||||
};
|
@ -1,229 +0,0 @@
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app })
|
||||
"https://builder.#{baseUrl}/v1/push?#{args}"
|
||||
|
||||
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
||||
querystring = require('querystring')
|
||||
args = querystring.stringify({ owner, app, buildId })
|
||||
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
||||
|
||||
formatImageName = (image) ->
|
||||
image.split('/').pop()
|
||||
|
||||
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!')
|
||||
|
||||
return [appName, options.build, source, image]
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
`resin deploy <appOwnerUsername>/<appName>`.
|
||||
|
||||
Note: If building with this command, all options supported by `resin build`
|
||||
are also supported with this command.
|
||||
|
||||
Examples:
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
'''
|
||||
permission: 'user'
|
||||
options: dockerUtils.appendOptions [
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Build image then deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'The source directory to use when building the image'
|
||||
alias: 's'
|
||||
},
|
||||
{
|
||||
signature: 'nologupload'
|
||||
description: "Don't upload build logs to the dashboard with image (if building)"
|
||||
boolean: true
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Logger = require('../utils/logger')
|
||||
logger = new Logger()
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
logs = ''
|
||||
|
||||
upload = (token, username, url) ->
|
||||
dockerUtils.getDocker(options)
|
||||
.then (docker) ->
|
||||
# Check input parameters
|
||||
parseInput(params, options)
|
||||
.then ([appName, build, source, imageName]) ->
|
||||
tmpNameAsync()
|
||||
.then (bufferFile) ->
|
||||
|
||||
# Setup the build args for how the build routine expects them
|
||||
options = _.assign({}, options, { appName })
|
||||
params = _.assign({}, params, { source })
|
||||
|
||||
Promise.try ->
|
||||
if build
|
||||
dockerUtils.runBuild(params, options, getBundleInfo, logger)
|
||||
else
|
||||
{ image: imageName, log: '' }
|
||||
.then ({ image: imageName, log: buildLogs }) ->
|
||||
logger.logInfo('Initializing 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 ''
|
||||
|
||||
logger.logInfo('Uploading logs to dashboard...')
|
||||
|
||||
Promise.join(
|
||||
logs
|
||||
token
|
||||
url
|
||||
buildId
|
||||
username
|
||||
params.appName
|
||||
uploadLogs
|
||||
)
|
||||
.return('Successfully uploaded logs')
|
||||
.then (msg) ->
|
||||
logger.logSuccess(msg) if msg isnt ''
|
||||
.asCallback(done)
|
||||
|
||||
Promise.join(
|
||||
resin.auth.getToken()
|
||||
resin.auth.whoami()
|
||||
resin.settings.get('resinUrl')
|
||||
upload
|
||||
)
|
317
lib/actions/deploy.js
Normal file
317
lib/actions/deploy.js
Normal file
@ -0,0 +1,317 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Imported here because it's needed for the setup
|
||||
// of this action
|
||||
import * as Bluebird from 'bluebird';
|
||||
|
||||
import * as dockerUtils from '../utils/docker';
|
||||
import * as compose from '../utils/compose';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
|
||||
/*
|
||||
Opts must be an object with the following keys:
|
||||
|
||||
app: the application instance to deploy to
|
||||
image: the image to deploy; optional
|
||||
dockerfilePath: name of an alternative Dockerfile; optional
|
||||
shouldPerformBuild
|
||||
shouldUploadLogs
|
||||
buildEmulated
|
||||
buildOpts: arguments to forward to docker build command
|
||||
*/
|
||||
const deployProject = function (docker, logger, composeOpts, opts) {
|
||||
const _ = require('lodash');
|
||||
const doodles = require('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
deployProject: $deployProject,
|
||||
loadProject,
|
||||
} = require('../utils/compose_ts');
|
||||
|
||||
return Bluebird.resolve(loadProject(logger, composeOpts, opts.image))
|
||||
.then(function (project) {
|
||||
if (
|
||||
project.descriptors.length > 1 &&
|
||||
!opts.app.application_type?.[0]?.supports_multicontainer
|
||||
) {
|
||||
throw new Error(
|
||||
'Target application does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
// find which services use images that already exist locally
|
||||
return (
|
||||
Bluebird.map(project.descriptors, function (d) {
|
||||
// unconditionally build (or pull) if explicitly requested
|
||||
if (opts.shouldPerformBuild) {
|
||||
return d;
|
||||
}
|
||||
return docker
|
||||
.getImage(typeof d.image === 'string' ? d.image : d.image.tag)
|
||||
.inspect()
|
||||
.return(d.serviceName)
|
||||
.catchReturn();
|
||||
})
|
||||
.filter((d) => !!d)
|
||||
.then(function (servicesToSkip) {
|
||||
// multibuild takes in a composition and always attempts to
|
||||
// build or pull all services. we workaround that here by
|
||||
// passing a modified composition.
|
||||
const compositionToBuild = _.cloneDeep(project.composition);
|
||||
compositionToBuild.services = _.omit(
|
||||
compositionToBuild.services,
|
||||
servicesToSkip,
|
||||
);
|
||||
if (_.size(compositionToBuild.services) === 0) {
|
||||
logger.logInfo(
|
||||
'Everything is up to date (use --build to force a rebuild)',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
return compose
|
||||
.buildProject(
|
||||
docker,
|
||||
logger,
|
||||
project.path,
|
||||
project.name,
|
||||
compositionToBuild,
|
||||
opts.app.arch,
|
||||
opts.app.device_type,
|
||||
opts.buildEmulated,
|
||||
opts.buildOpts,
|
||||
composeOpts.inlineLogs,
|
||||
composeOpts.convertEol,
|
||||
composeOpts.dockerfilePath,
|
||||
composeOpts.nogitignore,
|
||||
)
|
||||
.then((builtImages) => _.keyBy(builtImages, 'serviceName'));
|
||||
})
|
||||
.then((builtImages) =>
|
||||
project.descriptors.map(
|
||||
(d) =>
|
||||
builtImages[d.serviceName] ?? {
|
||||
serviceName: d.serviceName,
|
||||
name: typeof d.image === 'string' ? d.image : d.image.tag,
|
||||
logs: 'Build skipped; image for service already exists.',
|
||||
props: {},
|
||||
},
|
||||
),
|
||||
)
|
||||
// @ts-ignore slightly different return types of partial vs non-partial release
|
||||
.then(function (images) {
|
||||
if (opts.app.application_type?.[0]?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
return Bluebird.join(
|
||||
docker,
|
||||
logger,
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
appName: opts.appName,
|
||||
imageName: images[0].name,
|
||||
buildLogs: images[0].logs,
|
||||
shouldUploadLogs: opts.shouldUploadLogs,
|
||||
},
|
||||
deployLegacy,
|
||||
).then((releaseId) =>
|
||||
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
|
||||
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to
|
||||
// Promise.join typings
|
||||
sdk.models.release.get(releaseId, { $select: ['commit'] }),
|
||||
);
|
||||
}
|
||||
return Bluebird.join(
|
||||
sdk.auth.getUserId(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
(userId, auth, apiEndpoint) =>
|
||||
$deployProject(
|
||||
docker,
|
||||
logger,
|
||||
project.composition,
|
||||
images,
|
||||
opts.app.id,
|
||||
userId,
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
),
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(function (release) {
|
||||
logger.outputDeferredMessages();
|
||||
logger.logSuccess('Deploy succeeded!');
|
||||
logger.logSuccess(`Release: ${release.commit}`);
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
})
|
||||
.tapCatch(() => {
|
||||
logger.logError('Deploy failed');
|
||||
});
|
||||
};
|
||||
|
||||
export const deploy = {
|
||||
signature: 'deploy <appName> [image]',
|
||||
description:
|
||||
'Deploy a single image or a multicontainer project to a balena application',
|
||||
help: `\
|
||||
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
||||
|
||||
Use this command to deploy an image or a complete multicontainer project to an
|
||||
application, optionally building it first. The source images are searched for
|
||||
(and optionally built) using the docker daemon in your development machine or
|
||||
balena device. (See also the \`balena push\` command for the option of building
|
||||
the image in the balenaCloud build servers.)
|
||||
|
||||
Unless an image is specified, this command will look into the current directory
|
||||
(or the one specified by --source) for a docker-compose.yml file. If one is
|
||||
found, this command will deploy each service defined in the compose file,
|
||||
building it first if an image for it doesn't exist. If a compose file isn't
|
||||
found, the command will look for a Dockerfile[.template] file (or alternative
|
||||
Dockerfile specified with the \`-f\` option), and if yet that isn't found, it
|
||||
will try to generate one.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
|
||||
When --build is used, all options supported by \`balena build\` are also supported
|
||||
by this command.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
${dockerignoreHelp}
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena deploy myApp
|
||||
$ balena deploy myApp --build --source myBuildDir/
|
||||
$ balena deploy myApp myApp/myImage\
|
||||
`,
|
||||
permission: 'user',
|
||||
primary: true,
|
||||
options: dockerUtils.appendOptions(
|
||||
compose.appendOptions([
|
||||
{
|
||||
signature: 'source',
|
||||
parameter: 'source',
|
||||
description:
|
||||
'Specify an alternate source directory; default is the working directory',
|
||||
alias: 's',
|
||||
},
|
||||
{
|
||||
signature: 'build',
|
||||
boolean: true,
|
||||
description: 'Force a rebuild before deploy',
|
||||
alias: 'b',
|
||||
},
|
||||
{
|
||||
signature: 'nologupload',
|
||||
description:
|
||||
"Don't upload build logs to the dashboard with image (if building)",
|
||||
boolean: true,
|
||||
},
|
||||
]),
|
||||
),
|
||||
action(params, options) {
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
require('events').defaultMaxListeners = 1000;
|
||||
const sdk = getBalenaSdk();
|
||||
const {
|
||||
getRegistrySecrets,
|
||||
validateProjectDirectory,
|
||||
} = require('../utils/compose_ts');
|
||||
const helpers = require('../utils/helpers');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
const logger = Logger.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
// when Capitano converts a positional parameter (but not an option)
|
||||
// to a number, the original value is preserved with the _raw suffix
|
||||
let { appName, appName_raw, image } = params;
|
||||
|
||||
// look into "balena build" options if appName isn't given
|
||||
appName = appName_raw || appName || options.application;
|
||||
delete options.application;
|
||||
|
||||
return Bluebird.try(function () {
|
||||
if (appName == null) {
|
||||
throw new ExpectedError(
|
||||
'Please specify the name of the application to deploy',
|
||||
);
|
||||
}
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
'Build option is not applicable when specifying an image',
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(function () {
|
||||
if (image) {
|
||||
return getRegistrySecrets(sdk, options['registry-secrets']).then(
|
||||
(registrySecrets) => {
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
}).then(function ({ dockerfilePath, registrySecrets }) {
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => helpers.getAppWithArch(appName))
|
||||
.then(function (app) {
|
||||
return Bluebird.join(
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
compose.generateOpts(options),
|
||||
(docker, buildOpts, composeOpts) =>
|
||||
deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
buildOpts,
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
@ -1,446 +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')
|
||||
_ = require('lodash')
|
||||
|
||||
exports.list =
|
||||
signature: 'devices'
|
||||
description: 'list all devices'
|
||||
help: '''
|
||||
Use this command to list all devices that belong to you.
|
||||
|
||||
You can filter the devices by application by using the `--application` option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices
|
||||
$ resin devices --application MyApp
|
||||
$ resin devices --app MyApp
|
||||
$ resin devices -a MyApp
|
||||
'''
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.device.getAllByApplication(options.application)
|
||||
return resin.models.device.getAll()
|
||||
|
||||
.tap (devices) ->
|
||||
devices = _.map devices, (device) ->
|
||||
device.uuid = device.uuid.slice(0, 7)
|
||||
return device
|
||||
|
||||
console.log visuals.table.horizontal devices, [
|
||||
'id'
|
||||
'uuid'
|
||||
'name'
|
||||
'device_type'
|
||||
'application_name'
|
||||
'status'
|
||||
'is_online'
|
||||
'supervisor_version'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.info =
|
||||
signature: 'device <uuid>'
|
||||
description: 'list a single device'
|
||||
help: '''
|
||||
Use this command to show information about a single device.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device 7cf02a6
|
||||
'''
|
||||
permission: 'user'
|
||||
primary: true
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
|
||||
resin.models.device.getStatus(device).then (status) ->
|
||||
device.status = status
|
||||
|
||||
console.log visuals.table.vertical device, [
|
||||
"$#{device.name}$"
|
||||
'id'
|
||||
'device_type'
|
||||
'status'
|
||||
'is_online'
|
||||
'ip_address'
|
||||
'application_name'
|
||||
'last_seen'
|
||||
'uuid'
|
||||
'commit'
|
||||
'supervisor_version'
|
||||
'is_web_accessible'
|
||||
'note'
|
||||
'os_version'
|
||||
'dashboard_url'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.supported =
|
||||
signature: 'devices supported'
|
||||
description: 'list all supported devices'
|
||||
help: '''
|
||||
Use this command to get the list of all supported devices
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin devices supported
|
||||
'''
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
resin.models.config.getDeviceTypes().then (deviceTypes) ->
|
||||
console.log visuals.table.horizontal deviceTypes, [
|
||||
'slug'
|
||||
'name'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.register =
|
||||
signature: 'device register <application>'
|
||||
description: 'register a device'
|
||||
help: '''
|
||||
Use this command to register a device to an application.
|
||||
|
||||
Note that device api keys are only supported on ResinOS 2.0.3+
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device register MyApp
|
||||
$ resin device register MyApp --uuid <uuid>
|
||||
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'uuid'
|
||||
description: 'custom uuid'
|
||||
parameter: 'uuid'
|
||||
alias: 'u'
|
||||
}
|
||||
commandOptions.optionalDeviceApiKey
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.join(
|
||||
resin.models.application.get(params.application)
|
||||
options.uuid ? resin.models.device.generateUniqueKey()
|
||||
options.deviceApiKey ? resin.models.device.generateUniqueKey()
|
||||
(application, uuid, deviceApiKey) ->
|
||||
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)
|
||||
)
|
||||
.get('uuid')
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'device rm <uuid>'
|
||||
description: 'remove a device'
|
||||
help: '''
|
||||
Use this command to remove a device from resin.io.
|
||||
|
||||
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
|
||||
'''
|
||||
options: [ commandOptions.yes ]
|
||||
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 device?').then ->
|
||||
resin.models.device.remove(params.uuid)
|
||||
.nodeify(done)
|
||||
|
||||
exports.identify =
|
||||
signature: 'device identify <uuid>'
|
||||
description: 'identify a device with a UUID'
|
||||
help: '''
|
||||
Use this command to identify a device.
|
||||
|
||||
In the Raspberry Pi, the ACT led is blinked several times.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device identify 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.identify(params.uuid).nodeify(done)
|
||||
|
||||
exports.reboot =
|
||||
signature: 'device reboot <uuid>'
|
||||
description: 'restart a device'
|
||||
help: '''
|
||||
Use this command to remotely reboot a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin 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)
|
||||
|
||||
exports.shutdown =
|
||||
signature: 'device shutdown <uuid>'
|
||||
description: 'shutdown a device'
|
||||
help: '''
|
||||
Use this command to remotely shutdown a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin 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)
|
||||
|
||||
exports.enableDeviceUrl =
|
||||
signature: 'device public-url enable <uuid>'
|
||||
description: 'enable public URL for a device'
|
||||
help: '''
|
||||
Use this command to enable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url enable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.enableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.disableDeviceUrl =
|
||||
signature: 'device public-url disable <uuid>'
|
||||
description: 'disable public URL for a device'
|
||||
help: '''
|
||||
Use this command to disable public URL for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url disable 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.disableDeviceUrl(params.uuid).nodeify(done)
|
||||
|
||||
exports.getDeviceUrl =
|
||||
signature: 'device public-url <uuid>'
|
||||
description: 'gets the public URL of a device'
|
||||
help: '''
|
||||
Use this command to get the public URL of a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.getDeviceUrl(params.uuid).then (url) ->
|
||||
console.log(url)
|
||||
.nodeify(done)
|
||||
|
||||
exports.hasDeviceUrl =
|
||||
signature: 'device public-url status <uuid>'
|
||||
description: 'Returns true if public URL is enabled for a device'
|
||||
help: '''
|
||||
Use this command to determine if public URL is enabled for a device
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device public-url status 23c73a1
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
resin.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
|
||||
console.log(hasDeviceUrl)
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'device rename <uuid> [newName]'
|
||||
description: 'rename a resin device'
|
||||
help: '''
|
||||
Use this command to rename a device.
|
||||
|
||||
If you omit the name, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device rename 7cf02a6
|
||||
$ resin device rename 7cf02a6 MyPi
|
||||
'''
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
form = require('resin-cli-form')
|
||||
|
||||
Promise.try ->
|
||||
return params.newName if not _.isEmpty(params.newName)
|
||||
|
||||
form.ask
|
||||
message: 'How do you want to name this device?'
|
||||
type: 'input'
|
||||
|
||||
.then(_.partial(resin.models.device.rename, params.uuid))
|
||||
.nodeify(done)
|
||||
|
||||
exports.move =
|
||||
signature: 'device move <uuid>'
|
||||
description: 'move a device to another application'
|
||||
help: '''
|
||||
Use this command to move a device to another application you own.
|
||||
|
||||
If you omit the application, you'll get asked for it interactively.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device move 7cf02a6
|
||||
$ resin device move 7cf02a6 --application MyNewApp
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.optionalApplication ]
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
resin.models.device.get(params.uuid).then (device) ->
|
||||
return options.application or patterns.selectApplication (application) ->
|
||||
return _.every [
|
||||
application.device_type is device.device_type
|
||||
device.application_name isnt application.app_name
|
||||
]
|
||||
.tap (application) ->
|
||||
return resin.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'
|
||||
help: '''
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
Notice this command may ask for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin device init
|
||||
$ resin device init --application MyApp
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.yes
|
||||
commandOptions.advancedConfig
|
||||
_.assign({}, commandOptions.osVersion, { signature: 'os-version', parameter: 'os-version' })
|
||||
commandOptions.drive
|
||||
{
|
||||
signature: 'config'
|
||||
description: 'path to the config JSON file, see `resin 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')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
Promise.try ->
|
||||
return options.application if options.application?
|
||||
return patterns.selectApplication()
|
||||
.then(resin.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}")
|
||||
.disposer (tempPath) ->
|
||||
return rimraf(tempPath)
|
||||
|
||||
Promise.using download(), (tempPath) ->
|
||||
capitanoRunAsync("device register #{application.app_name}")
|
||||
.then(resin.models.device.get)
|
||||
.tap (device) ->
|
||||
configureCommand = "os configure '#{tempPath}' #{device.uuid}"
|
||||
if options.config
|
||||
configureCommand += " --config '#{options.config}'"
|
||||
else if options.advanced
|
||||
configureCommand += ' --advanced'
|
||||
capitanoRunAsync(configureCommand)
|
||||
.then ->
|
||||
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
|
||||
if options.yes
|
||||
osInitCommand += ' --yes'
|
||||
if options.drive
|
||||
osInitCommand += " --drive #{options.drive}"
|
||||
capitanoRunAsync(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 ->
|
||||
throw error
|
||||
.then (device) ->
|
||||
console.log('Done')
|
||||
return device.uuid
|
||||
|
||||
.nodeify(done)
|
114
lib/actions/device.js
Normal file
114
lib/actions/device.js
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as commandOptions from './command-options';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
export const init = {
|
||||
signature: 'device init',
|
||||
description: 'initialise a device with balenaOS',
|
||||
help: `\
|
||||
Use this command to download the OS image of a certain application and write it to an SD Card.
|
||||
|
||||
Notice this command may ask for confirmation interactively.
|
||||
You can avoid this by passing the \`--yes\` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena device init
|
||||
$ balena device init --application MyApp\
|
||||
`,
|
||||
options: [
|
||||
commandOptions.optionalApplication,
|
||||
commandOptions.yes,
|
||||
commandOptions.advancedConfig,
|
||||
_.assign({}, commandOptions.osVersionOrSemver, {
|
||||
signature: 'os-version',
|
||||
parameter: 'os-version',
|
||||
}),
|
||||
commandOptions.drive,
|
||||
{
|
||||
signature: 'config',
|
||||
description: 'path to the config JSON file, see `balena os build-config`',
|
||||
parameter: 'config',
|
||||
},
|
||||
],
|
||||
permission: 'user',
|
||||
action(_params, options) {
|
||||
const Bluebird = require('bluebird');
|
||||
const rimraf = Bluebird.promisify(require('rimraf'));
|
||||
const tmp = require('tmp');
|
||||
const tmpNameAsync = Bluebird.promisify(tmp.tmpName);
|
||||
tmp.setGracefulCleanup();
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = require('../utils/patterns');
|
||||
const { runCommand } = require('../utils/helpers');
|
||||
|
||||
return Bluebird.try(function () {
|
||||
if (options.application != null) {
|
||||
return options.application;
|
||||
}
|
||||
return patterns.selectApplication();
|
||||
})
|
||||
.then(balena.models.application.get)
|
||||
.then(function (application) {
|
||||
const download = () =>
|
||||
tmpNameAsync()
|
||||
.then(function (tempPath) {
|
||||
const osVersion = options['os-version'] || 'default';
|
||||
return runCommand(
|
||||
`os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`,
|
||||
);
|
||||
})
|
||||
.disposer((tempPath) => rimraf(tempPath));
|
||||
|
||||
return Bluebird.using(download(), (tempPath) =>
|
||||
runCommand(`device register ${application.app_name}`)
|
||||
.then(balena.models.device.get)
|
||||
.tap(function (device) {
|
||||
let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`;
|
||||
if (options.config) {
|
||||
configureCommand += ` --config '${options.config}'`;
|
||||
} else if (options.advanced) {
|
||||
configureCommand += ' --advanced';
|
||||
}
|
||||
return runCommand(configureCommand)
|
||||
.then(function () {
|
||||
let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`;
|
||||
if (options.yes) {
|
||||
osInitCommand += ' --yes';
|
||||
}
|
||||
if (options.drive) {
|
||||
osInitCommand += ` --drive ${options.drive}`;
|
||||
}
|
||||
return runCommand(osInitCommand);
|
||||
})
|
||||
.catch((error) =>
|
||||
balena.models.device.remove(device.uuid).finally(function () {
|
||||
throw error;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
).then(function (device) {
|
||||
console.log('Done');
|
||||
return device.uuid;
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
@ -1,182 +0,0 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
commandOptions = require('./command-options')
|
||||
|
||||
exports.list =
|
||||
signature: 'envs'
|
||||
description: 'list all environment variables'
|
||||
help: '''
|
||||
Use this command to list all environment variables for
|
||||
a particular application or device.
|
||||
|
||||
This command lists all custom environment variables.
|
||||
If you want to see all environment variables, including private
|
||||
ones used by resin, use the verbose option.
|
||||
|
||||
Example:
|
||||
|
||||
$ resin envs --application MyApp
|
||||
$ resin envs --application MyApp --verbose
|
||||
$ resin envs --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
|
||||
{
|
||||
signature: 'verbose'
|
||||
description: 'show private environment variables'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
}
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
_ = require('lodash')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
visuals = require('resin-cli-visuals')
|
||||
|
||||
Promise.try ->
|
||||
if options.application?
|
||||
return resin.models.environmentVariables.getAllByApplication(options.application)
|
||||
else if options.device?
|
||||
return resin.models.environmentVariables.device.getAll(options.device)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
|
||||
.tap (environmentVariables) ->
|
||||
if _.isEmpty(environmentVariables)
|
||||
throw new Error('No environment variables found')
|
||||
if not options.verbose
|
||||
isSystemVariable = resin.models.environmentVariables.isSystemVariable
|
||||
environmentVariables = _.reject(environmentVariables, isSystemVariable)
|
||||
|
||||
console.log visuals.table.horizontal environmentVariables, [
|
||||
'id'
|
||||
'name'
|
||||
'value'
|
||||
]
|
||||
.nodeify(done)
|
||||
|
||||
exports.remove =
|
||||
signature: 'env rm <id>'
|
||||
description: 'remove an environment variable'
|
||||
help: '''
|
||||
Use this command to remove an environment variable from an application.
|
||||
|
||||
Don't remove resin specific variables, as things might not work as expected.
|
||||
|
||||
Notice this command asks for confirmation interactively.
|
||||
You can avoid this by passing the `--yes` boolean option.
|
||||
|
||||
If you want to eliminate a device environment variable, pass the `--device` boolean option.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rm 215
|
||||
$ resin env rm 215 --yes
|
||||
$ resin env rm 215 --device
|
||||
'''
|
||||
options: [
|
||||
commandOptions.yes
|
||||
commandOptions.booleanDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
patterns = require('../utils/patterns')
|
||||
|
||||
patterns.confirm(options.yes, 'Are you sure you want to delete the environment variable?').then ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.remove(params.id)
|
||||
else
|
||||
resin.models.environmentVariables.remove(params.id)
|
||||
.nodeify(done)
|
||||
|
||||
exports.add =
|
||||
signature: 'env add <key> [value]'
|
||||
description: 'add an environment variable'
|
||||
help: '''
|
||||
Use this command to add an enviroment variable to an application.
|
||||
|
||||
If value is omitted, the tool will attempt to use the variable's value
|
||||
as defined in your host machine.
|
||||
|
||||
Use the `--device` option if you want to assign the environment variable
|
||||
to a specific device.
|
||||
|
||||
If the value is grabbed from the environment, a warning message will be printed.
|
||||
Use `--quiet` to remove it.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env add EDITOR vim --application MyApp
|
||||
$ resin env add TERM --application MyApp
|
||||
$ resin env add EDITOR vim --device 7cf02a6
|
||||
'''
|
||||
options: [
|
||||
commandOptions.optionalApplication
|
||||
commandOptions.optionalDevice
|
||||
]
|
||||
permission: 'user'
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if not params.value?
|
||||
params.value = process.env[params.key]
|
||||
|
||||
if not params.value?
|
||||
throw new Error("Environment value not found for key: #{params.key}")
|
||||
else
|
||||
console.info("Warning: using #{params.key}=#{params.value} from host environment")
|
||||
|
||||
if options.application?
|
||||
resin.models.environmentVariables.create(options.application, params.key, params.value)
|
||||
else if options.device?
|
||||
resin.models.environmentVariables.device.create(options.device, params.key, params.value)
|
||||
else
|
||||
throw new Error('You must specify an application or device')
|
||||
.nodeify(done)
|
||||
|
||||
exports.rename =
|
||||
signature: 'env rename <id> <value>'
|
||||
description: 'rename an environment variable'
|
||||
help: '''
|
||||
Use this command to rename an enviroment variable from an application.
|
||||
|
||||
Pass the `--device` boolean option if you want to rename a device environment variable.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin env rename 376 emacs
|
||||
$ resin env rename 376 emacs --device
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [ commandOptions.booleanDevice ]
|
||||
action: (params, options, done) ->
|
||||
Promise = require('bluebird')
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
Promise.try ->
|
||||
if options.device
|
||||
resin.models.environmentVariables.device.update(params.id, params.value)
|
||||
else
|
||||
resin.models.environmentVariables.update(params.id, params.value)
|
||||
.nodeify(done)
|
@ -1,124 +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')
|
||||
capitano = require('capitano')
|
||||
columnify = require('columnify')
|
||||
messages = require('../utils/messages')
|
||||
|
||||
parse = (object) ->
|
||||
return _.fromPairs _.map object, (item) ->
|
||||
|
||||
# Hacky way to determine if an object is
|
||||
# a function or a command
|
||||
if item.alias?
|
||||
signature = item.toString()
|
||||
else
|
||||
signature = item.signature.toString()
|
||||
|
||||
return [
|
||||
signature
|
||||
item.description
|
||||
]
|
||||
|
||||
indent = (text) ->
|
||||
text = _.map text.split('\n'), (line) ->
|
||||
return ' ' + line
|
||||
return text.join('\n')
|
||||
|
||||
print = (data) ->
|
||||
console.log indent columnify data,
|
||||
showHeaders: false
|
||||
minWidth: 35
|
||||
|
||||
general = (params, options, done) ->
|
||||
console.log('Usage: resin [COMMAND] [OPTIONS]\n')
|
||||
console.log(messages.reachingOut)
|
||||
console.log('\nPrimary commands:\n')
|
||||
|
||||
# We do not want the wildcard command
|
||||
# to be printed in the help screen.
|
||||
commands = _.reject capitano.state.commands, (command) ->
|
||||
return command.hidden or command.isWildcard()
|
||||
|
||||
groupedCommands = _.groupBy commands, (command) ->
|
||||
if command.plugin
|
||||
return 'plugins'
|
||||
if command.primary
|
||||
return 'primary'
|
||||
return 'secondary'
|
||||
|
||||
print(parse(groupedCommands.primary))
|
||||
|
||||
if options.verbose
|
||||
if not _.isEmpty(groupedCommands.plugins)
|
||||
console.log('\nInstalled plugins:\n')
|
||||
print(parse(groupedCommands.plugins))
|
||||
|
||||
console.log('\nAdditional commands:\n')
|
||||
print(parse(groupedCommands.secondary))
|
||||
else
|
||||
console.log('\nRun `resin help --verbose` to list additional commands')
|
||||
|
||||
if not _.isEmpty(capitano.state.globalOptions)
|
||||
console.log('\nGlobal Options:\n')
|
||||
print(parse(capitano.state.globalOptions))
|
||||
|
||||
return done()
|
||||
|
||||
command = (params, options, done) ->
|
||||
capitano.state.getMatchCommand params.command, (error, command) ->
|
||||
return done(error) if error?
|
||||
|
||||
if not command? or command.isWildcard()
|
||||
return done(new Error("Command not found: #{params.command}"))
|
||||
|
||||
console.log("Usage: #{command.signature}")
|
||||
|
||||
if command.help?
|
||||
console.log("\n#{command.help}")
|
||||
else if command.description?
|
||||
console.log("\n#{_.capitalize(command.description)}")
|
||||
|
||||
if not _.isEmpty(command.options)
|
||||
console.log('\nOptions:\n')
|
||||
print(parse(command.options))
|
||||
|
||||
return done()
|
||||
|
||||
exports.help =
|
||||
signature: 'help [command...]'
|
||||
description: 'show help'
|
||||
help: '''
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin help apps
|
||||
$ resin help os download
|
||||
'''
|
||||
primary: true
|
||||
options: [
|
||||
signature: 'verbose'
|
||||
description: 'show additional commands'
|
||||
boolean: true
|
||||
alias: 'v'
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
if params.command?
|
||||
command(params, options, done)
|
||||
else
|
||||
general(params, options, done)
|
190
lib/actions/help.js
Normal file
190
lib/actions/help.js
Normal file
@ -0,0 +1,190 @@
|
||||
/*
|
||||
Copyright 2016-2020 Balena Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as capitano from 'capitano';
|
||||
import * as columnify from 'columnify';
|
||||
import * as messages from '../utils/messages';
|
||||
import { getManualSortCompareFunction } from '../utils/helpers';
|
||||
import { exitWithExpectedError } from '../errors';
|
||||
import { getOclifHelpLinePairs } from './help_ts';
|
||||
|
||||
const parse = (object) =>
|
||||
_.map(object, function (item) {
|
||||
// Hacky way to determine if an object is
|
||||
// a function or a command
|
||||
let signature;
|
||||
if (item.alias != null) {
|
||||
signature = item.toString();
|
||||
} else {
|
||||
signature = item.signature.toString();
|
||||
}
|
||||
|
||||
return [signature, item.description];
|
||||
});
|
||||
|
||||
const indent = function (text) {
|
||||
text = _.map(text.split('\n'), (line) => ' ' + line);
|
||||
return text.join('\n');
|
||||
};
|
||||
|
||||
const print = (usageDescriptionPairs) =>
|
||||
console.log(
|
||||
indent(
|
||||
columnify(_.fromPairs(usageDescriptionPairs), {
|
||||
showHeaders: false,
|
||||
minWidth: 35,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const manuallySortedPrimaryCommands = [
|
||||
'help',
|
||||
'login',
|
||||
'push',
|
||||
'logs',
|
||||
'ssh',
|
||||
'apps',
|
||||
'app',
|
||||
'devices',
|
||||
'device',
|
||||
'tunnel',
|
||||
'preload',
|
||||
'build',
|
||||
'deploy',
|
||||
'join',
|
||||
'leave',
|
||||
'local scan',
|
||||
];
|
||||
|
||||
const general = function (_params, options, done) {
|
||||
console.log('Usage: balena [COMMAND] [OPTIONS]\n');
|
||||
|
||||
console.log('Primary commands:\n');
|
||||
|
||||
// We do not want the wildcard command
|
||||
// to be printed in the help screen.
|
||||
const commands = capitano.state.commands.filter(
|
||||
(command) => !command.hidden && !command.isWildcard(),
|
||||
);
|
||||
|
||||
const capitanoCommands = _.groupBy(commands, function (command) {
|
||||
if (command.primary) {
|
||||
return 'primary';
|
||||
}
|
||||
return 'secondary';
|
||||
});
|
||||
|
||||
return getOclifHelpLinePairs()
|
||||
.then(function (oclifHelpLinePairs) {
|
||||
const primaryHelpLinePairs = parse(capitanoCommands.primary)
|
||||
.concat(oclifHelpLinePairs.primary)
|
||||
.sort(
|
||||
getManualSortCompareFunction(manuallySortedPrimaryCommands, function (
|
||||
[signature],
|
||||
manualItem,
|
||||
) {
|
||||
return (
|
||||
signature === manualItem || signature.startsWith(`${manualItem} `)
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const secondaryHelpLinePairs = parse(capitanoCommands.secondary)
|
||||
.concat(oclifHelpLinePairs.secondary)
|
||||
.sort();
|
||||
|
||||
print(primaryHelpLinePairs);
|
||||
|
||||
if (options.verbose) {
|
||||
console.log('\nAdditional commands:\n');
|
||||
print(secondaryHelpLinePairs);
|
||||
} else {
|
||||
console.log(
|
||||
'\nRun `balena help --verbose` to list additional commands',
|
||||
);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(capitano.state.globalOptions)) {
|
||||
console.log('\nGlobal Options:\n');
|
||||
print(parse(capitano.state.globalOptions).sort());
|
||||
}
|
||||
console.log(indent('--debug\n'));
|
||||
|
||||
console.log(messages.help);
|
||||
|
||||
return done();
|
||||
})
|
||||
.catch(done);
|
||||
};
|
||||
|
||||
const commandHelp = (params, _options, done) =>
|
||||
capitano.state.getMatchCommand(params.command, function (error, command) {
|
||||
if (error != null) {
|
||||
return done(error);
|
||||
}
|
||||
|
||||
if (command == null || command.isWildcard()) {
|
||||
exitWithExpectedError(`Command not found: ${params.command}`);
|
||||
}
|
||||
|
||||
console.log(`Usage: ${command.signature}`);
|
||||
|
||||
if (command.help != null) {
|
||||
console.log(`\n${command.help}`);
|
||||
} else if (command.description != null) {
|
||||
console.log(`\n${_.capitalize(command.description)}`);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(command.options)) {
|
||||
console.log('\nOptions:\n');
|
||||
print(parse(command.options).sort());
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
return done();
|
||||
});
|
||||
|
||||
export const help = {
|
||||
signature: 'help [command...]',
|
||||
description: 'show help',
|
||||
help: `\
|
||||
Get detailed help for an specific command.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena help apps
|
||||
$ balena help os download\
|
||||
`,
|
||||
primary: true,
|
||||
options: [
|
||||
{
|
||||
signature: 'verbose',
|
||||
description: 'show additional commands',
|
||||
boolean: true,
|
||||
alias: 'v',
|
||||
},
|
||||
],
|
||||
action(params, options, done) {
|
||||
if (params.command != null) {
|
||||
return commandHelp(params, options, done);
|
||||
} else {
|
||||
return general(params, options, done);
|
||||
}
|
||||
},
|
||||
};
|
61
lib/actions/help_ts.ts
Normal file
61
lib/actions/help_ts.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @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 Command from '../command';
|
||||
|
||||
import { capitanoizeOclifUsage } from '../utils/oclif-utils';
|
||||
|
||||
export async function getOclifHelpLinePairs() {
|
||||
const { convertedCommands } = await import('../preparser');
|
||||
const primary: Array<[string, string]> = [];
|
||||
const secondary: Array<[string, string]> = [];
|
||||
|
||||
for (const convertedCmd of convertedCommands) {
|
||||
const [topic, cmd] = convertedCmd.split(':');
|
||||
const pathComponents = ['..', 'actions-oclif', topic];
|
||||
if (cmd) {
|
||||
pathComponents.push(cmd);
|
||||
}
|
||||
|
||||
const cmdModule = await import(path.join(...pathComponents));
|
||||
const command: typeof Command = cmdModule.default;
|
||||
|
||||
if (!command.hidden) {
|
||||
if (command.primary) {
|
||||
primary.push(getCmdUsageDescriptionLinePair(command));
|
||||
} else {
|
||||
secondary.push(getCmdUsageDescriptionLinePair(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { primary, secondary };
|
||||
}
|
||||
|
||||
function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] {
|
||||
const usage = capitanoizeOclifUsage(cmd.usage);
|
||||
let description = '';
|
||||
// note: [^] matches any characters (including line breaks), achieving the
|
||||
// same effect as the 's' regex flag which is only supported by Node 9+
|
||||
const matches = /\s*([^]+?)\n[^]*/.exec(cmd.description || '');
|
||||
if (matches && matches.length > 1) {
|
||||
description = _.lowerFirst(_.trimEnd(matches[1], '.'));
|
||||
}
|
||||
return [usage, description];
|
||||
}
|
@ -1,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.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
wizard: require('./wizard')
|
||||
app: require('./app')
|
||||
info: require('./info')
|
||||
auth: require('./auth')
|
||||
device: require('./device')
|
||||
env: require('./environment-variables')
|
||||
keys: require('./keys')
|
||||
logs: require('./logs')
|
||||
local: require('./local')
|
||||
notes: require('./notes')
|
||||
help: require('./help')
|
||||
os: require('./os')
|
||||
settings: require('./settings')
|
||||
config: require('./config')
|
||||
sync: require('./sync')
|
||||
ssh: require('./ssh')
|
||||
internal: require('./internal')
|
||||
build: require('./build')
|
||||
deploy: require('./deploy')
|
||||
util: require('./util')
|
||||
preload: require('./preload')
|
@ -1,5 +1,5 @@
|
||||
###
|
||||
Copyright 2016-2017 Resin.io
|
||||
/*
|
||||
Copyright 2016-2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -12,24 +12,21 @@ 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.
|
||||
###
|
||||
*/
|
||||
|
||||
validEmail = require('@resin.io/valid-email')
|
||||
export * as config from './config';
|
||||
export * as device from './device';
|
||||
export * as help from './help';
|
||||
export * as local from './local';
|
||||
export * as logs from './logs';
|
||||
export * as os from './os';
|
||||
export * as push from './push';
|
||||
export * as ssh from './ssh';
|
||||
export * as tunnel from './tunnel';
|
||||
export * as util from './util';
|
||||
|
||||
exports.validateEmail = (input) ->
|
||||
if not validEmail(input)
|
||||
return 'Email is not valid'
|
||||
export { build } from './build';
|
||||
|
||||
return true
|
||||
export { deploy } from './deploy';
|
||||
|
||||
exports.validatePassword = (input) ->
|
||||
if input.length < 8
|
||||
return 'Password should be 8 characters long'
|
||||
|
||||
return true
|
||||
|
||||
exports.validateApplicationName = (input) ->
|
||||
if input.length < 4
|
||||
return 'The application name should be at least 4 characters'
|
||||
|
||||
return true
|
||||
export { preload } from './preload';
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user