mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-24 18:45:07 +00:00
Compare commits
982 Commits
v12.32.1
...
allow-cust
Author | SHA1 | Date | |
---|---|---|---|
eed4a385a9 | |||
9e4dd3fce2 | |||
b2590136fc | |||
bf5e61a61c | |||
f550d0c596 | |||
54302669b8 | |||
a4a4e33d7b | |||
8d6a621bfb | |||
4b2602676b | |||
b0810c0f85 | |||
97a6013537 | |||
1ba8db1459 | |||
cdada0aec8 | |||
1166533482 | |||
01538728cd | |||
3a7f6d78b0 | |||
dce48c90e9 | |||
fe70d164c1 | |||
09e2550b32 | |||
07854c3d42 | |||
858a455501 | |||
4e5eb4bcee | |||
696bad3ed6 | |||
9a9d0f02ef | |||
f46d00640b | |||
e369bd3599 | |||
75b29112a7 | |||
b7b01ecd53 | |||
801a25995c | |||
8296dea78c | |||
1da5a75c14 | |||
166de57179 | |||
85dece9e95 | |||
bfbc71215c | |||
d243c14d74 | |||
804eb27551 | |||
4266dc6951 | |||
0ba3522584 | |||
19b0e9489d | |||
d9fed9c34c | |||
81ee9f397f | |||
b9722c6796 | |||
29ade0f696 | |||
d5ae612513 | |||
65ba63d1a8 | |||
f5ffa7d84f | |||
dac3ace61d | |||
72459a04d1 | |||
1e83fcf1e3 | |||
b8769bb9e9 | |||
9f52ee8b21 | |||
90b65cd06b | |||
72a924f00e | |||
e4624eda10 | |||
4173cd82e6 | |||
b393f27e1b | |||
1a4a0e2439 | |||
4cd8f4c16e | |||
2de9d526e5 | |||
d9427c3c59 | |||
fc0cfac475 | |||
99094dbfda | |||
0711eefb7c | |||
dc40b0d969 | |||
4b5def0a8a | |||
f44fa38113 | |||
167dfeb269 | |||
a816548bb5 | |||
94001efc81 | |||
8bfafe8ecc | |||
d78045b6ab | |||
11eabc4b96 | |||
bfaa91c752 | |||
1b615e4690 | |||
7954e13154 | |||
45d8872a82 | |||
56cff46408 | |||
47a1a9c6af | |||
a434a5e657 | |||
221c213791 | |||
d2150c5cb7 | |||
70e113152d | |||
4a64102d67 | |||
9bf267166e | |||
f12249bc81 | |||
80d6c71b02 | |||
9ef4117fb8 | |||
25f3bf1fbe | |||
fe65351666 | |||
7d13946c3e | |||
1ffa8d38f1 | |||
6914d39370 | |||
04db9c7a91 | |||
c785d01a1c | |||
87ba364f89 | |||
b7e5915c7a | |||
0cef6b8f87 | |||
71f1dbd80a | |||
36edcf0cb8 | |||
8204dcad93 | |||
03bcb6cff7 | |||
66b6eed57c | |||
99b0f2c022 | |||
947a9bade7 | |||
15ea601d68 | |||
ce888966dd | |||
3fc6eb7a47 | |||
73aebf10bf | |||
292c372eb4 | |||
dc3261d9c7 | |||
301d0ab3a0 | |||
efe0a018d7 | |||
fa2a232e5f | |||
da1f022df9 | |||
8e6a6b81d9 | |||
77906c4152 | |||
26bc68753b | |||
af6b263f7a | |||
a27e216e44 | |||
50e1efa448 | |||
519ac0383a | |||
3d0ef9bc4f | |||
49e23464f9 | |||
a1c9b4b80e | |||
2b1be3e5d9 | |||
e46378ec51 | |||
27ee9c85e7 | |||
21b6ec46e3 | |||
817ce5dc96 | |||
d9af28bca7 | |||
8646be7979 | |||
14ba287e0d | |||
1671e46d99 | |||
507333c463 | |||
8b320d3e9e | |||
e1be268749 | |||
1a0019e6d0 | |||
e79cdb671f | |||
f38e643cf0 | |||
b8e190cd1d | |||
9cca654bd5 | |||
35177e2d2f | |||
1a24b193e7 | |||
272915192b | |||
96774f4c52 | |||
a034f585ba | |||
365d95c36b | |||
c6313c08ae | |||
f5764c4659 | |||
aff094575b | |||
4aaaf64f8d | |||
7b88ce273f | |||
b011af89ad | |||
1bf8c1bfe7 | |||
2b39d5d111 | |||
98663af7f6 | |||
5628824bee | |||
d12d7996bc | |||
0dcf4cbff6 | |||
884e37d242 | |||
f4a24e26c3 | |||
122eccf3dc | |||
bd598788dc | |||
406482b4da | |||
a381c97ca9 | |||
8ce78ba33c | |||
f53f148c89 | |||
0086feb645 | |||
4ee55b049f | |||
90c6f121cc | |||
d3c27ae859 | |||
8f39c1de6c | |||
4df1831187 | |||
2bce761ace | |||
d78b76aceb | |||
f07f6b84d4 | |||
d297a10570 | |||
9d0b82122a | |||
338477463a | |||
d1275760fa | |||
0f4054fa4d | |||
7545fc5d6e | |||
a1f25809cb | |||
e0a3c4bd95 | |||
d843e75512 | |||
72c57608d5 | |||
d9de7636db | |||
10b5af6967 | |||
51c050c725 | |||
eb52c47de5 | |||
4b1378dfbc | |||
1a77d86347 | |||
bd5188f4b9 | |||
034f459bfa | |||
bc405d997e | |||
af27cf2cbe | |||
83b9bf67c2 | |||
abd73b805b | |||
37bfd4db98 | |||
be74143d5f | |||
9975e5d9ac | |||
1341413966 | |||
1a5b914a6f | |||
c5e8f0d6ea | |||
3a143fe413 | |||
3445e4a08e | |||
166130c3df | |||
c3a8a905f7 | |||
2b878e87d8 | |||
063e9d40f0 | |||
2b58143164 | |||
861d4f33b7 | |||
81f4aae7d2 | |||
46ab335407 | |||
07cb0cbfcd | |||
a2392dc580 | |||
8b3235ab2b | |||
15dac6f194 | |||
3c93db8449 | |||
9d8df0b781 | |||
bcadbdbed8 | |||
05a96fa60e | |||
2e37536e7a | |||
025c4ef7f2 | |||
ecbc660bf5 | |||
ba1f17d537 | |||
3ab8f7500e | |||
0a25bec010 | |||
01e765e670 | |||
61844f2386 | |||
46aa08c953 | |||
b6c7fb82c3 | |||
fcda09009a | |||
1a6fe1f3de | |||
98e91c0607 | |||
bed2387d83 | |||
50e852acee | |||
da30623e4e | |||
7a46b367a7 | |||
d9651c7393 | |||
e371b1e759 | |||
77cf4af166 | |||
9d197317ca | |||
9a8b0b4a0d | |||
0c62b9ef08 | |||
83a5e7392a | |||
f0c8c37022 | |||
ba26d3204d | |||
d53542975e | |||
632296a271 | |||
3e089fcdb2 | |||
d61c300750 | |||
a0a97c5f40 | |||
165f3b83ca | |||
5bf95300ee | |||
adb460b270 | |||
ca80bd52fe | |||
281f8abb9a | |||
2cf2918d73 | |||
7dfb7474f5 | |||
6ee0b48c9a | |||
bd01fbf90c | |||
cd19845b6b | |||
5545883c3f | |||
75a380b0ba | |||
35fe7c6a58 | |||
69249b3139 | |||
bf897fd56d | |||
150c6e75f5 | |||
e8bc43dc64 | |||
1213689de2 | |||
c1017e8e27 | |||
7ad9e685f6 | |||
c778aaffaf | |||
b98047cacf | |||
03ace6e4b2 | |||
9b4701bcb7 | |||
174312977a | |||
963d9af817 | |||
af5ec51232 | |||
1cd9fbf6a0 | |||
72639e9e59 | |||
447dcc1480 | |||
564716faa7 | |||
3e5b4457c2 | |||
793e70d909 | |||
5761a306be | |||
adff0f2a0a | |||
4ec45a0c43 | |||
ecf4b046b5 | |||
b0cae93ac9 | |||
53b66678d4 | |||
0b9b65ef88 | |||
8a84d9d792 | |||
c535b8e1ea | |||
234fb6cd39 | |||
8714830b48 | |||
0e07b36691 | |||
ba80d3c38c | |||
e65dc82cfe | |||
bc727521c6 | |||
a8c0c884d3 | |||
b11c7157d3 | |||
578de7bcd4 | |||
cfc6b3ce9e | |||
1c7a354fe7 | |||
40a0941ca3 | |||
0ab4760272 | |||
42b2269e81 | |||
c818d846b3 | |||
3328f40416 | |||
58d10c1908 | |||
2fd0ca6a02 | |||
173028fd0d | |||
62d5bf4436 | |||
63a0d19770 | |||
8244636bf2 | |||
6a01fb361c | |||
ca637b3fb6 | |||
006293bd01 | |||
338b5d79d3 | |||
60dd0daae5 | |||
662b8283a6 | |||
cfc866cf41 | |||
e566badfff | |||
69834c417e | |||
8aa9c62afd | |||
4f29e37fe7 | |||
99e8a36bb5 | |||
669cbe227f | |||
e9156d77f1 | |||
767216c842 | |||
d3018f9061 | |||
37c6ad855b | |||
ca97678358 | |||
3bb0036ba8 | |||
ac9e2a9e7e | |||
52e95e6d0a | |||
c5d2aa7eec | |||
683220e303 | |||
44f09b32fa | |||
d1a0660a3d | |||
ee1987f188 | |||
39e9997d9e | |||
97b8c75043 | |||
7cb8349f29 | |||
6063f4c776 | |||
4899d545f1 | |||
115bf6433d | |||
e5ce1ade89 | |||
9c4174ea8a | |||
cf16957195 | |||
4de369ff95 | |||
ac3ebff8ee | |||
76b01d92d3 | |||
19144163ee | |||
535ffccbad | |||
6f5ada9692 | |||
1c7d9255ae | |||
807e6ea2ad | |||
c76f019fd0 | |||
3c2c925eed | |||
14b54be15e | |||
7fb82f7447 | |||
4a5d44a0f1 | |||
1cba0284df | |||
6e4fe229bf | |||
7033075900 | |||
ded268ff3c | |||
a366f0b7eb | |||
507c8a1bfd | |||
1fb46bfa5d | |||
2e115968d5 | |||
83020797b0 | |||
0c4647e980 | |||
a20d2a04a8 | |||
57b0dccc7d | |||
d1e3bdf29a | |||
bdf7fedd7a | |||
c163662f4a | |||
a2823fd3ec | |||
d717352b84 | |||
e46902e683 | |||
e96ef6697e | |||
6f54197b7b | |||
34b4ac2d9f | |||
f99244603a | |||
523c0af0fb | |||
2206b475c6 | |||
a117dc0382 | |||
cf3e8ff909 | |||
36d1af1e33 | |||
18f83092fe | |||
ee3c796787 | |||
934c3ddf38 | |||
66e6daf78c | |||
97eb107de4 | |||
def205f1fb | |||
5c8f78678b | |||
769f1ca5b4 | |||
cb26a736fc | |||
d28847d5aa | |||
c0902bb119 | |||
26aae0afab | |||
5f3cf75c1a | |||
8a7fbdb55d | |||
b260f80bcc | |||
9ec37975f3 | |||
73c487c2f5 | |||
3cb35ea318 | |||
efe6fd22ce | |||
6ee8d8a899 | |||
c735f13636 | |||
edb0fdc3c1 | |||
14a07ac7f7 | |||
264cd94be5 | |||
2664f4e7fb | |||
3ce2653881 | |||
719860366f | |||
21ded85c7a | |||
c91f67d27e | |||
18eedfec7f | |||
1fe0480a8a | |||
c7f56d92dd | |||
a92f58134f | |||
cc6a8ef76e | |||
88f4a3d88e | |||
f6d668684a | |||
be7c0dc897 | |||
566b7f97e0 | |||
f55dd81a19 | |||
dba5349390 | |||
6a8dfcc664 | |||
59e35d866f | |||
9235c928f1 | |||
3d88f0144a | |||
a6b461ba91 | |||
b96da951db | |||
8235cead07 | |||
30b9d9141d | |||
03b41d9989 | |||
aab3af2153 | |||
600457de61 | |||
17db857e10 | |||
eb45ae2a30 | |||
2eaf70bff3 | |||
226f45f732 | |||
c4990f3a26 | |||
0195a3b18c | |||
3d90aeb122 | |||
0571039bfe | |||
ee668a4c5c | |||
ead4dbfab1 | |||
0b498d09df | |||
2b2c40c22d | |||
ba3a3865b5 | |||
f8402bc40c | |||
c667ffa8eb | |||
6d6065ddf5 | |||
44f55f8e7b | |||
d2c77760b3 | |||
7496710c85 | |||
be6a468507 | |||
88835e63bd | |||
3572cb3cd6 | |||
7fbd1de063 | |||
a4ab07cd08 | |||
9185eaa2b7 | |||
ff3abe1fba | |||
1ac3b70b81 | |||
e946178953 | |||
6589589bee | |||
6ae598b55e | |||
915f7e3763 | |||
cd17d79067 | |||
7e4f4392e9 | |||
3c0e998616 | |||
bd1bf8153d | |||
f2528dcd18 | |||
ec26433925 | |||
43cddd2e5d | |||
eeb2be2912 | |||
3bf8befb1d | |||
948095ce4d | |||
d2330f9ed1 | |||
cc19b00998 | |||
ed5ac75a10 | |||
465b8a1b5e | |||
eccadbdcb9 | |||
31eb734af1 | |||
fa7b59d64f | |||
1e42bfa0d5 | |||
5464e550e7 | |||
c0f27a663d | |||
d1c61c62ab | |||
a9691bff57 | |||
f5d09a43cd | |||
d11e547e11 | |||
bd462aee02 | |||
f633c0468b | |||
e4f61a1242 | |||
96142a002e | |||
6b9a5cd89c | |||
ba2d3d60ec | |||
d1e66bc1a5 | |||
58799915a9 | |||
5f2d55f569 | |||
8d6e51391c | |||
8454b02988 | |||
879d98ef98 | |||
c4e317a290 | |||
7ca4d2d720 | |||
e1e88ec56d | |||
33f7fa3829 | |||
3d516e7c5f | |||
a8507508b7 | |||
008972b3d3 | |||
92b86330a0 | |||
2563c07c6a | |||
1d4b949cf3 | |||
d17e02a930 | |||
a355cbaa79 | |||
bd021c0a2d | |||
a80f676804 | |||
f723c58089 | |||
e27a4e2e31 | |||
b91b72c408 | |||
5cf84d3f1d | |||
7d58b8c120 | |||
851301a336 | |||
ec6fd050f6 | |||
6f81053882 | |||
dbd8a9a08c | |||
256f1abf1b | |||
acd352cb3c | |||
31f927c27c | |||
3d0f16168a | |||
b2d932afab | |||
398175f0b3 | |||
2fb9c6c773 | |||
66608b32e9 | |||
c403683edf | |||
1e6ab46ca3 | |||
02d3220f2d | |||
c86cdc8f84 | |||
84f02dc063 | |||
9145f2fb28 | |||
1164388d78 | |||
06f6094401 | |||
67e11467f7 | |||
c8dfd0ca65 | |||
8b110a835a | |||
7564d95f82 | |||
f12f2b79ef | |||
176d731f9e | |||
1ed39d1d37 | |||
580ca0d584 | |||
73572df7cf | |||
23b42b1a2b | |||
632322e3c2 | |||
4faa5d7f57 | |||
9b967592a9 | |||
e01483cd2b | |||
6d89ff4bbf | |||
126e731117 | |||
32d26ad074 | |||
2bcfec9d0f | |||
c04e63ab7d | |||
79be06820c | |||
ffb94c380f | |||
385b5e9ec6 | |||
8d3a4343cb | |||
6eeb16245b | |||
3961060f90 | |||
a6dfc9126a | |||
e7ddd07b7b | |||
fea351d960 | |||
40e0b2dbed | |||
3def4d0e4a | |||
aa286cc0e7 | |||
8abeb6aed7 | |||
f285880135 | |||
2b5c387313 | |||
8babf4c908 | |||
bfc995e948 | |||
c6a0bc0fba | |||
ae69accf0f | |||
cfcace4c99 | |||
6e07db0813 | |||
5c40c8d51f | |||
d827005154 | |||
76081343cc | |||
f3fb9b6bdf | |||
c125e0b38d | |||
73b2f6b4b1 | |||
fdc0d08e96 | |||
e431a59af7 | |||
41a2dbe60c | |||
6ba67eefdb | |||
3b885ad906 | |||
5574dc0318 | |||
fcea91bfb6 | |||
7316c4e075 | |||
389b7a1463 | |||
09d004423c | |||
97978ff812 | |||
498e21f0ab | |||
257dd514ed | |||
85cbdd4947 | |||
73625611da | |||
d2a5a9ba86 | |||
1cd78215e0 | |||
6d744d0b07 | |||
9d312bcd12 | |||
e22aa847e3 | |||
0d1ca67d5b | |||
c4a5a25f03 | |||
b183d88400 | |||
2b6a2142eb | |||
58b29bf4bb | |||
fc0903a414 | |||
cea23f5d5e | |||
5a9b5e3b08 | |||
52138d41eb | |||
5acdc63068 | |||
b546e4dd97 | |||
e4870916e2 | |||
3ca93448cd | |||
f66395e2d5 | |||
952d782e90 | |||
d53c9b3c50 | |||
2f706c0200 | |||
d64b6deb81 | |||
55fc9b2ade | |||
6c29d0ae27 | |||
f46452f6de | |||
c166ec7597 | |||
7325c79888 | |||
2a29b386eb | |||
23b07f8a41 | |||
6d641b4841 | |||
7b498149b1 | |||
ae5ea0f4e8 | |||
f635f648da | |||
3d4e2cf823 | |||
ef3b630887 | |||
19040ccb6c | |||
8e712ac910 | |||
c401ed35ac | |||
94be97313b | |||
48053ecefc | |||
cc60e86507 | |||
bd774e8553 | |||
c493c33e38 | |||
9487b33144 | |||
befdae1b90 | |||
08dfc945f3 | |||
8791c2f4e1 | |||
be306e6a20 | |||
6cfff72c59 | |||
adae718c2e | |||
132e1a63b2 | |||
a18e182ae4 | |||
e098cdca17 | |||
b42af74983 | |||
8bb211e441 | |||
ffccbfba12 | |||
56c1af50c0 | |||
8b9e3ccdc8 | |||
de95262f93 | |||
ed49938504 | |||
52ad0f6a57 | |||
7f6738c73c | |||
88fc3f7714 | |||
1afb29b923 | |||
09a4e8db2d | |||
6c81440428 | |||
3eca65ce0d | |||
6319b9dc13 | |||
290acaecbb | |||
305c9045f0 | |||
b701151769 | |||
e03bbb7275 | |||
3fd66c39ae | |||
b30075a18b | |||
a4fc95e99b | |||
63d8e5e6a3 | |||
6244af3464 | |||
8773927b3f | |||
29a3fd40a2 | |||
d6faf060e6 | |||
352fd197b7 | |||
afb6f938b7 | |||
d3adbcdba9 | |||
33fce1f24f | |||
ab90a5f150 | |||
a8b2212fed | |||
6bb8df30dd | |||
0327ed766d | |||
1009958340 | |||
5ce17ea70f | |||
9c821511b1 | |||
d793335287 | |||
dc59b7e4b0 | |||
370b844538 | |||
a8c2724929 | |||
09dd2dd354 | |||
f3ab41841a | |||
3dee30a0fe | |||
d34073f695 | |||
24fe6666e4 | |||
3fd5981085 | |||
08ee8643cb | |||
8db36ccec9 | |||
deb3e4c4ac | |||
a8ff21af69 | |||
4c54d6c171 | |||
83f213c007 | |||
d0cdc900a2 | |||
9937b91606 | |||
972c2470c5 | |||
7d568a928b | |||
2331e0a3e5 | |||
cb9b6be24b | |||
c2d3eee7cc | |||
d8b08f7272 | |||
819bdac354 | |||
318de8f017 | |||
2b0341e12a | |||
21f7463607 | |||
19fd3094d1 | |||
7c4974f4f5 | |||
3b56ed278e | |||
254ef1c8cf | |||
d11f49e0f8 | |||
48d7d0ef5e | |||
c7bbbc4159 | |||
d2fabcaf30 | |||
e137c2aed2 | |||
58704b08d3 | |||
485a9e944f | |||
1d7a50f007 | |||
64a44e7a5f | |||
c3406603db | |||
f1fa187a58 | |||
6cb2893750 | |||
216172ed4f | |||
3717d8cc0f | |||
8338e2e933 | |||
918c2e912d | |||
be0622ec80 | |||
07eef7bb49 | |||
0892caa155 | |||
fa4e8e7b55 | |||
e624726e44 | |||
f914fa2d8a | |||
c8f5542c8a | |||
a2cad7bf53 | |||
3a871a0003 | |||
e552e36f7b | |||
c325f1158e | |||
f79ccc0c95 | |||
1ec8d9a4ca | |||
427b0d9b41 | |||
cfd790a193 | |||
36f4c1312b | |||
fe7cbf4f74 | |||
4e8b8fe582 | |||
2986e6cea3 | |||
bb6b4b255a | |||
350c4abb96 | |||
fec96b41ee | |||
1dba5cc7c1 | |||
43c6fe672f | |||
486cae1aaa | |||
4d588e51a7 | |||
0035545ce1 | |||
d559b9a5a1 | |||
e2ffc5f068 | |||
75b2fa0e9e | |||
c619ecd41b | |||
7ed01a925b | |||
460022a7cf | |||
d15b54cf40 | |||
c938df2445 | |||
bf1df05606 | |||
e04242db64 | |||
9265588745 | |||
cd8070b1a6 | |||
b17dad8c60 | |||
a254e46118 | |||
6e7a0defb7 | |||
492dbae7f1 | |||
ccc2c20b6d | |||
501882fd26 | |||
e2ff561728 | |||
5544f4a5dd | |||
a3e90182bc | |||
3ac85dcc5f | |||
1ac573c659 | |||
2c922ee6d2 | |||
d9821939d9 | |||
732fc2d539 | |||
535a443d7c | |||
579d68a8f0 | |||
ffac8cb9e5 | |||
0f2780744f | |||
b4495839ca | |||
f45ac42dd3 | |||
112a7b8194 | |||
fa26004648 | |||
ba1ea54d69 | |||
9fb62d92b7 | |||
8780a24fb5 | |||
3d3e91d49d | |||
f6e6d9ce8b | |||
0f9d78ab50 | |||
06f7683837 | |||
83a23d9f30 | |||
ffa181a2c3 | |||
d50d18d492 | |||
0b0fb94834 | |||
c1244c0c98 | |||
213e54feb1 | |||
cc8a8513e9 | |||
42c3236313 | |||
91fd515266 | |||
57cd096612 | |||
854501cf8d | |||
d44afa8c39 | |||
b7500fc2c2 | |||
dc6c8d7472 | |||
5c5be8f7b7 | |||
5bdd6c6034 | |||
a5bade99fc | |||
9c3eb76856 | |||
973f1a9c40 | |||
16ea0c9d6d | |||
73bfe545e8 | |||
f53e658ca2 | |||
b66706e8ee | |||
11e50466d5 | |||
431c4b6e4a | |||
d12490f816 | |||
67b7b8b5d0 | |||
16d1f0f06f | |||
9676ea94cb | |||
df8ce0bbe0 | |||
6437bb7511 | |||
ac96616e4e | |||
2737c9c53c | |||
3b8a46f523 | |||
3ac1994941 | |||
b3a6c6cb0f | |||
6d4faa7b2c | |||
9036ce9af3 | |||
4911db640f | |||
e7999f52a9 | |||
68b61e7424 | |||
329b84d01e | |||
25b1dff5d8 | |||
fb1768b4ca | |||
cbc1e52256 | |||
37c2880996 | |||
835445be2e | |||
52fe7481fc | |||
88072173d0 | |||
fdc2bff063 | |||
4f6f20f469 | |||
50af0760ce | |||
43906d22c8 | |||
43f1188f1d | |||
2629a01c7f | |||
5fc009a6ae | |||
480f84993b | |||
d1fdbd927e | |||
4bfd345b68 | |||
d4a153d2ee | |||
3cff091e3a | |||
b2ad9f1643 | |||
f7623bef85 | |||
af63794571 | |||
65d5bdff08 | |||
23165806aa | |||
3649bafbb1 | |||
c62445a399 | |||
b233ea3e3e | |||
4fe660b3a5 | |||
1f07cd1b1c | |||
bcea5193a1 | |||
8b99cd7170 | |||
1986c9339c | |||
b90c9b0d7e | |||
e28c3f9814 | |||
d054ced541 | |||
c8e4d2c9a6 | |||
9671372b9e | |||
2a4ff75203 | |||
f3d750a024 | |||
a701cd8d4d | |||
e2c0c2f359 | |||
15fc805f89 | |||
0a995ecc49 | |||
1ba992ada2 | |||
e47fd0c887 | |||
af1de34840 | |||
96fb525378 | |||
3d1f16c0ab | |||
6fb58a25fc | |||
e6b85c9cf8 | |||
43b93e7fd4 | |||
a05dcf08b8 | |||
9636985ee7 | |||
023fc57914 | |||
492bdab2fe | |||
941c365259 | |||
fed58278c9 | |||
d74af38bfe | |||
53926067ca | |||
7181dc5401 | |||
e35e13f9a7 | |||
6e0638f3be | |||
d60ec13d5c | |||
731e50a757 | |||
b363d28664 | |||
7ae83d9ce5 | |||
31281549a6 | |||
e86bcc438c | |||
a1cf602f6f | |||
4cd3ef8b91 | |||
e4eb4586f5 | |||
360c6e42f8 | |||
f76702c4e0 | |||
d3586696b4 | |||
f73e3db4de | |||
1f74889386 | |||
743de66138 | |||
8d56fe9678 | |||
3d9d8bf5c8 | |||
8c3df9ae30 | |||
e71184ed3a | |||
caadce6c2b | |||
f45fac6138 | |||
aeff5997d0 | |||
b5028c65cc | |||
f69276e7c9 | |||
9fff9266d4 | |||
0e7f953f72 | |||
61b11994b5 | |||
1e1935cfb1 | |||
27e2b03702 | |||
358acbd2c8 | |||
b040a21268 | |||
074fe010bd | |||
34557e35ee | |||
3bff569758 | |||
cf06a8dfad | |||
584aa745f7 | |||
194d12cb3d | |||
7739379444 | |||
5c93df921e | |||
da652c6bce | |||
1cd341e6cd | |||
9d2884aab7 | |||
f128eaf389 | |||
70b0524eb6 | |||
c898747468 | |||
6fc3b0df58 | |||
746676beb9 | |||
611f59a0da | |||
c6430274e5 | |||
9637f75617 | |||
439d8391ee | |||
0d3ca63f00 | |||
1f3677bdb2 | |||
10bca728f0 | |||
9763a14e97 | |||
fe24280adf | |||
a11f9ec705 | |||
836ae1cf4a | |||
b4d37e7a3a | |||
055ad834e7 |
37
.dockerignore
Normal file
37
.dockerignore
Normal file
@ -0,0 +1,37 @@
|
||||
# Reminders:
|
||||
# * Matching rules are different to `.gitignore`
|
||||
# * A pattern without '**' matches in the project's root directory only
|
||||
# * Leading and trailing '/' are discarded (it is not possible to
|
||||
# distinguish between files and directories)
|
||||
# * More details: https://github.com/balena-io-modules/dockerignore
|
||||
|
||||
# development and testing tools or IDEs
|
||||
**/*.log
|
||||
**/*.pid
|
||||
**/*.seed
|
||||
.idea
|
||||
.lock-wscript
|
||||
.nvmrc
|
||||
.nyc_output
|
||||
.vscode
|
||||
coverage
|
||||
lib-cov
|
||||
logs
|
||||
pids
|
||||
|
||||
# OS cache files
|
||||
**/.DS_Store
|
||||
|
||||
# balena CLI config and build files
|
||||
**/.balenaconf
|
||||
**/.fast-boot.json
|
||||
**/.resinconf
|
||||
balenarc.yml
|
||||
build
|
||||
build-bin
|
||||
dist
|
||||
node_modules
|
||||
oclif.manifest.json
|
||||
package-lock.json
|
||||
resinrc.yml
|
||||
tmp
|
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
/completion/*
|
||||
/bin/*
|
21
.eslintrc.js
Normal file
21
.eslintrc.js
Normal file
@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@balena/lint/config/.eslintrc.js'],
|
||||
parserOptions: {
|
||||
project: 'tsconfig.dev.json',
|
||||
},
|
||||
root: true,
|
||||
rules: {
|
||||
ignoreDefinitionFiles: 0,
|
||||
// to avoid the `warning Forbidden non-null assertion @typescript-eslint/no-non-null-assertion`
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: ['resin-cli-visuals', 'chalk', 'common-tags', 'resin-cli-form'],
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
};
|
6
.gitattributes
vendored
6
.gitattributes
vendored
@ -4,9 +4,13 @@
|
||||
*.* -eol
|
||||
|
||||
*.sh text eol=lf
|
||||
.dockerignore eol=lf
|
||||
Dockerfile eol=lf
|
||||
Dockerfile.* eol=lf
|
||||
* text=auto 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
|
||||
docs/balena-cli.md text eol=lf
|
||||
# crlf for the eol conversion test files
|
||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
||||
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
* @balena-io/balena-cli
|
10
.github/ISSUE_TEMPLATE.md
vendored
10
.github/ISSUE_TEMPLATE.md
vendored
@ -32,11 +32,11 @@ Please describe what actually happened instead:
|
||||
Examples:
|
||||
|
||||
```
|
||||
balena push myApp
|
||||
balena push myFleet
|
||||
balena push 192.168.0.12
|
||||
balena deploy myApp
|
||||
balena deploy myApp --build
|
||||
balena build . -a myApp
|
||||
balena deploy myFleet
|
||||
balena deploy myFleet --build
|
||||
balena build . -f myFleet
|
||||
balena build . -A armv7hf -d raspberrypi3
|
||||
```
|
||||
|
||||
@ -48,7 +48,7 @@ additional information. The `--logs` option reveals additional information for t
|
||||
|
||||
```
|
||||
balena build . --logs
|
||||
balena deploy myApp --build --logs
|
||||
balena deploy myFleet --build --logs
|
||||
```
|
||||
|
||||
# Steps to Reproduce the Problem
|
||||
|
135
.github/actions/publish/action.yml
vendored
Normal file
135
.github/actions/publish/action.yml
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
---
|
||||
name: package and draft GitHub release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||
required: true
|
||||
secrets:
|
||||
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||
required: true
|
||||
variables:
|
||||
description: 'JSON stringified object containing all the variables from the calling workflow'
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
XCODE_APP_LOADER_EMAIL:
|
||||
type: string
|
||||
default: 'accounts+apple@balena.io'
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
choco install yq
|
||||
|
||||
- name: Install additional tools
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
brew install coreutils
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://github.com/Apple-Actions/import-codesign-certs
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
uses: apple-actions/import-codesign-certs@v1
|
||||
with:
|
||||
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
Import-PfxCertificate `
|
||||
-FilePath ${{ runner.temp }}/certificate.pfx `
|
||||
-CertStoreLocation Cert:\CurrentUser\My `
|
||||
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
|
||||
# https://github.com/product-os/scripts/tree/master/shared
|
||||
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
|
||||
- name: Package release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
|
||||
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [[ $runner_os =~ darwin|macos|osx ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
CSC_KEYCHAIN=signing_temp
|
||||
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
|
||||
elif [[ $runner_os =~ windows|win ]]; then
|
||||
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
|
||||
CSC_LINK='${{ runner.temp }}\certificate.pfx'
|
||||
|
||||
# patches/all/oclif.patch
|
||||
MSYSSHELLPATH="$(which bash)"
|
||||
MSYSTEM=MSYS
|
||||
|
||||
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
|
||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||
fi
|
||||
|
||||
npm run package
|
||||
|
||||
find dist -type f -maxdepth 1
|
||||
|
||||
env:
|
||||
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
|
||||
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
# https://sectigo.com/resource-library/time-stamping-server
|
||||
TIMESTAMP_SERVER: http://timestamp.sectigo.com
|
||||
# Apple notarization (automation/build-bin.ts)
|
||||
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
XCODE_APP_LOADER_TEAM_ID: ${{ inputs.XCODE_APP_LOADER_TEAM_ID }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ strategy.job-index }}
|
||||
path: dist
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
65
.github/actions/test/action.yml
vendored
Normal file
65
.github/actions/test/action.yml
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
variables:
|
||||
description: "JSON stringified object containing all the variables from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.x'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Set up Python 3.11
|
||||
if: runner.os == 'macOS'
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Test release
|
||||
shell: bash
|
||||
run: |
|
||||
set -ea
|
||||
|
||||
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
|
||||
|
||||
if [[ -e package-lock.json ]] || [[ -e npm-shrinkwrap.json ]]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
|
||||
npm run build
|
||||
npm run test:core
|
||||
|
||||
- name: Compress custom source
|
||||
shell: pwsh
|
||||
run: tar --exclude-vcs -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
26
.github/workflows/flowzone.yml
vendored
Normal file
26
.github/workflows/flowzone.yml
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
name: Flowzone
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflow executions for pull_request and pull_request_target
|
||||
if: |
|
||||
(
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.event_name == 'pull_request'
|
||||
) || (
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
github.event_name == 'pull_request_target'
|
||||
)
|
||||
secrets: inherit
|
||||
with:
|
||||
custom_runs_on: '[["self-hosted","Linux","distro:focal","X64"],["self-hosted","Linux","distro:focal","ARM64"],["macos-12"],["windows-2019"]]'
|
||||
github_prerelease: false
|
||||
restrict_custom_actions: false
|
69
.gitignore
vendored
69
.gitignore
vendored
@ -1,47 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# Reminders:
|
||||
# * A pattern without '/' matches in subdirectories as well (files and directories)
|
||||
# * A leading '/' anchors matching to the directory where `.gitignore` is defined
|
||||
# * A trailing '/' makes the pattern match against directories only
|
||||
# More details: https://git-scm.com/docs/gitignore
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
# development and testing tools or IDEs
|
||||
*.log
|
||||
*.pid
|
||||
*.seed
|
||||
/.idea/
|
||||
/.lock-wscript
|
||||
/.nvmrc
|
||||
/.nyc_output/
|
||||
/.vscode/
|
||||
/coverage/
|
||||
/lib-cov/
|
||||
/logs
|
||||
/pids
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# Commenting this out is preferred by some people, see
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
|
||||
node_modules
|
||||
|
||||
package-lock.json
|
||||
.resinconf
|
||||
.balenaconf
|
||||
resinrc.yml
|
||||
balenarc.yml
|
||||
|
||||
# OS cache files
|
||||
.DS_Store
|
||||
.idea
|
||||
.nvmrc
|
||||
.vscode
|
||||
|
||||
/tmp
|
||||
build/
|
||||
build-bin/
|
||||
build-zip/
|
||||
dist/
|
||||
|
||||
# Ignore fast-boot cache file
|
||||
**/.fast-boot.json
|
||||
# balena CLI config and build files
|
||||
.balenaconf
|
||||
.fast-boot.json
|
||||
.resinconf
|
||||
/balenarc.yml
|
||||
/build/
|
||||
/build-bin/
|
||||
/dist/
|
||||
/node_modules
|
||||
/oclif.manifest.json
|
||||
/package-lock.json
|
||||
/resinrc.yml
|
||||
/tmp/
|
||||
|
@ -1,8 +1,10 @@
|
||||
module.exports = {
|
||||
spec: 'tests/commands/app/create.spec.ts',
|
||||
reporter: 'spec',
|
||||
require: 'ts-node/register/transpile-only',
|
||||
file: './tests/config-tests',
|
||||
timeout: 12000,
|
||||
// To test only, say, 'push.spec.ts', do it as follows so that
|
||||
// requests are authenticated:
|
||||
// spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],
|
||||
spec: 'tests/**/*.spec.ts',
|
||||
};
|
||||
|
20
.resinci.yml
20
.resinci.yml
@ -1,20 +0,0 @@
|
||||
---
|
||||
npm:
|
||||
platforms:
|
||||
- name: linux
|
||||
os: ubuntu
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
- name: linux
|
||||
os: alpine
|
||||
architecture: x86_64
|
||||
node_versions:
|
||||
- "10"
|
||||
- "12"
|
||||
- "14"
|
||||
|
||||
docker:
|
||||
publish: false
|
25
.travis.yml
25
.travis.yml
@ -1,25 +0,0 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
node_js:
|
||||
- "10"
|
||||
matrix:
|
||||
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: script
|
||||
script: npm run release
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
|
||||
repo: balena-io/balena-cli
|
File diff suppressed because it is too large
Load Diff
4261
CHANGELOG.md
4261
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -105,12 +105,12 @@ npm run update balena-sdk ^13.0.0 major
|
||||
|
||||
## Editing documentation files (README, INSTALL, Reference website...)
|
||||
|
||||
The `doc/cli.markdown` file is automatically generated by running `npm run build:doc` (which also
|
||||
The `docs/balena-cli.md` file is automatically generated by running `npm run build:doc` (which also
|
||||
runs as part of `npm run build`). That file is then pulled by scripts in the
|
||||
[balena-io/docs](https://github.com/balena-io/docs/) GitHub repo for publishing at the [CLI
|
||||
Documentation page](https://www.balena.io/docs/reference/cli/).
|
||||
|
||||
The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
The content sources for the auto generation of `docs/balena-cli.md` are:
|
||||
|
||||
* [Selected
|
||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||
@ -120,18 +120,69 @@ The content sources for the auto generation of `doc/cli.markdown` are:
|
||||
* `lib/commands/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
|
||||
`docs/balena-cli.md` by the `getCapitanoDoc()` function in
|
||||
[`automation/capitanodoc/capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts).
|
||||
|
||||
**IMPORTANT**
|
||||
|
||||
The file [`capitanodoc.ts`](https://github.com/balena-io/balena-cli/blob/master/automation/capitanodoc/capitanodoc.ts) lists
|
||||
commands to generate documentation from. At the moment, it's manually updated and maintained alphabetically.
|
||||
|
||||
To add a new command to be documented,
|
||||
|
||||
1. Find the resource which it is part of or create a new one.
|
||||
2. List the location of the build file
|
||||
3. Make sure to add your files in alphabetical order
|
||||
4. Resources with plural names needs to have 2 sections if they have commands like: "fleet, fleets" or "device, devices" or "tag, tags"
|
||||
|
||||
Once added, run the command `npm run build` to generate the documentation
|
||||
|
||||
The `INSTALL*.md` and `TROUBLESHOOTING.md` files are also manually edited.
|
||||
|
||||
## Patches folder
|
||||
|
||||
The `patches` folder contains patch files created with the
|
||||
[patch-package](https://www.npmjs.com/package/patch-package) tool. Small code changes to
|
||||
third-party modules can be made by directly editing Javascript files under the `node_modules`
|
||||
folder and then running `patch-package` to create the patch files. The patch files are then
|
||||
applied immediately after `npm install`, through the `postinstall` script defined in
|
||||
`package.json`.
|
||||
|
||||
The subfolders of the `patches` folder are documented in the
|
||||
[apply-patches.js](https://github.com/balena-io/balena-cli/blob/master/patches/apply-patches.js)
|
||||
script.
|
||||
|
||||
To make changes to the patch files under the `patches` folder, **do not edit them directly,**
|
||||
not even for a "single character change" because the hash values in the patch files also need
|
||||
to be recomputed by `patch-packages`. Instead, edit the relevant files under `node_modules`
|
||||
directly, and then run `patch-packages` with the `--patch-dir` option to specify the subfolder
|
||||
where the patch should be saved. For example, edit `node_modules/exit-hook/index.js` and then
|
||||
run:
|
||||
|
||||
```sh
|
||||
$ npx patch-package --patch-dir patches/all exit-hook
|
||||
```
|
||||
|
||||
That said, these kinds of patches should be avoided in favour of creating pull requests
|
||||
upstream. Patch files create additional maintenance work over time as the patches need to be
|
||||
updated when the dependencies are updated, and they prevent the compounding community benefit
|
||||
that sharing fixes upstream have on open source projects like the balena CLI. The typical
|
||||
scenario where these patches are used is when the upstream maintainers are unresponsive or
|
||||
unwilling to merge the required fixes, the fixes are very small and specific to the balena CLI,
|
||||
and creating a fork of the upstream repo is likely to be more long-term effort than maintaining
|
||||
the patches.
|
||||
|
||||
## Windows
|
||||
|
||||
The `npm run build:installer` script (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 changes are made to npm scripts in `package.json`,
|
||||
check that they also run on a standard Windows Command Prompt.
|
||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||
that produces the `.exe` graphical installer on Windows also requires
|
||||
[NSIS](https://sourceforge.net/projects/nsis/) and [MSYS2](https://www.msys2.org/) to be
|
||||
installed. Be sure to add `C:\Program Files (x86)\NSIS` to the PATH, so that `makensis`
|
||||
is available. MSYS2 is recommended when developing the balena CLI on Windows.
|
||||
|
||||
If changes are made to npm scripts in `package.json`, don't assume that a Unix shell like
|
||||
bash is available. For example, some Windows shells don't have the `cp` and `rm` commands,
|
||||
which is why you'll often find `ncp` and `rimraf` used in `package.json` scripts.
|
||||
|
||||
## Updating the 'npm-shrinkwrap.json' file
|
||||
|
||||
@ -164,6 +215,24 @@ Optionally, these steps may be automated by installing the
|
||||
npx npm-merge-driver install -g
|
||||
```
|
||||
|
||||
## `fast-boot` and `npm link` - modifying the `node_modules` folder
|
||||
|
||||
During development or debugging, it is sometimes useful to temporarily modify the `node_modules`
|
||||
folder (with or without making the respective changes to the `npm-shrinkwrap.json` file),
|
||||
replacing dependencies with different versions. This can be achieved with the `npm link`
|
||||
command, or by manually editing or copying files to the `node_modules` folder.
|
||||
|
||||
Unexpected behavior may then be observed because of the CLI's use of the
|
||||
[fast-boot2](https://www.npmjs.com/package/fast-boot2) package that caches module resolution.
|
||||
`fast-boot2` is configured in `lib/fast-boot.ts` to automatically invalidate the cache if
|
||||
changes are made to the `package.json` or `npm-shrinkwrap.json` files, but the cache won't
|
||||
be automatically invalidated if `npm link` is used or if manual modifications are made to the
|
||||
`node_modules` folder. In this situation:
|
||||
|
||||
* Manually delete the module cache file (typically `~/.balena/cli-module-cache.json`), or
|
||||
* Use the `bin/balena-dev` entry point (instead of `bin/balena`) as it does not activate
|
||||
`fast-boot2`.
|
||||
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
@ -237,3 +306,11 @@ gotchas to bear in mind:
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
## Further debugging notes
|
||||
|
||||
* If you need to selectively run specific tests, `it.only` will not work in cases when authorization is required as part of the test cycle. In order to target specific tests, control execution via `.mocharc.js` instead. Here is an example of targeting the `deploy` tests.
|
||||
|
||||
replace: `spec: 'tests/**/*.spec.ts',`
|
||||
|
||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||
|
@ -60,7 +60,7 @@ macOS: | `/usr/local/lib/balena-cli/` <br> `/usr/local/bin/balena`
|
||||
[Windows](https://www.computerhope.com/issues/ch000549.htm)
|
||||
|
||||
> * If you are using macOS 10.15 or later (Catalina, Big Sur), [check this known issue and
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/1479).
|
||||
> workaround](https://github.com/balena-io/balena-cli/issues/2244).
|
||||
> * **Linux Alpine** and **Busybox:** the standalone zip package is not currently compatible with
|
||||
> these "compact" Linux distributions, because of the alternative C libraries they ship with.
|
||||
> For these, consider the [NPM Installation](#npm-installation) option.
|
||||
@ -76,58 +76,76 @@ as described above.
|
||||
|
||||
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:
|
||||
some development tools to be installed first, as follows.
|
||||
|
||||
* [Node.js](https://nodejs.org/) version 10 (min **10.20.0**) 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`
|
||||
> **The balena CLI currently requires Node.js version 20.**
|
||||
> **Versions 21 and later are not yet fully supported.**
|
||||
|
||||
On **Windows (not WSL),** the dependencies above and additional ones can be met by installing:
|
||||
### Install development tools
|
||||
|
||||
* 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`
|
||||
#### **Linux or WSL** (Windows Subsystem for Linux)
|
||||
|
||||
```sh
|
||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
The `curl` command line above uses
|
||||
[nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script) to install
|
||||
Node.js, instead of using `apt-get`. Installing Node.js through `apt-get` is a common source of
|
||||
problems from permission errors to conflict with other system packages, and therefore not
|
||||
recommended.
|
||||
|
||||
#### **macOS**
|
||||
|
||||
* Download and install Apple's Command Line Tools from https://developer.apple.com/downloads/
|
||||
* Install Node.js through [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md#install--update-script):
|
||||
|
||||
```sh
|
||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||
$ . ~/.bashrc
|
||||
$ nvm install 20
|
||||
```
|
||||
|
||||
#### **Windows** (not WSL)
|
||||
|
||||
Install:
|
||||
|
||||
* If you'd like the ability to switch between Node.js versions, install
|
||||
- Node.js v20 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
||||
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||
instead.
|
||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||
* `pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based
|
||||
interactive CLI menus to misbehave. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* The Windows Driver Kit (WDK), which is needed to compile some native Node modules. It is **not**
|
||||
necessary to install Visual Studio, only the WDK, which is "step 2" in the following guides:
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-wdk-for-windows-10-version-1903)
|
||||
* [WDK for Windows 10](https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk#download-icon-step-2-install-refreshed-wdk-for-windows-10-version-2004)
|
||||
* [WDK for earlier versions of Windows](https://docs.microsoft.com/en-us/windows-hardware/drivers/other-wdk-downloads#step-2-install-the-wdk)
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package (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`
|
||||
* The [windows-build-tools](https://www.npmjs.com/package/windows-build-tools) npm package,
|
||||
by running the following command on an [administrator
|
||||
console](https://www.howtogeek.com/194041/how-to-open-the-command-prompt-as-administrator-in-windows-8.1/):
|
||||
`npm install --global --production windows-build-tools`
|
||||
|
||||
With these dependencies in place, the balena CLI installation command is:
|
||||
### Install the balena CLI
|
||||
|
||||
After installing the development tools, install the balena CLI with:
|
||||
|
||||
```sh
|
||||
$ npm install balena-cli -g --production --unsafe-perm
|
||||
$ npm install balena-cli --global --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).
|
||||
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
|
||||
container) in order to allow npm scripts like `postinstall` to be executed.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
The `balena ssh`, `scan`, `build`, `deploy`, `preload` and `os configure` commands may require
|
||||
The `balena ssh`, `scan`, `build`, `deploy` and `preload` commands may require
|
||||
additional software to be installed. Check the Additional Dependencies sections for each operating
|
||||
system:
|
||||
|
||||
@ -135,9 +153,9 @@ system:
|
||||
* [macOS](./INSTALL-MAC.md#additional-dependencies)
|
||||
* [Linux](./INSTALL-LINUX.md#additional-dependencies)
|
||||
|
||||
The `build` and `deploy` commands are also capable of using Docker or balenaEngine on a remote
|
||||
server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images)). Reasons why this
|
||||
Where Docker or balenaEngine are required, they may be installed on the local machine (where the
|
||||
balena CLI is executed), on a remote server, or on a balenaOS device running a [balenaOS development
|
||||
image](https://www.balena.io/docs/reference/OS/overview/2.x/#dev-vs-prod-images). Reasons why this
|
||||
may be desirable include:
|
||||
|
||||
* To avoid having to install Docker on the development machine / laptop.
|
||||
@ -145,6 +163,7 @@ may be desirable include:
|
||||
* To build or run images "natively" on an ARM device, avoiding the need for QEMU emulation.
|
||||
|
||||
To use a remote Docker Engine (daemon) or balenaEngine, specify the remote machine's IP address and
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. For more details,
|
||||
check `balena help build` or the [online
|
||||
port number with the `--dockerHost` and `--dockerPort` command-line options. The `preload` command
|
||||
has additional requirements because the bind mount feature is used. For more details, see
|
||||
`balena help` for each command or the [online
|
||||
reference](https://www.balena.io/docs/reference/cli/#cli-command-reference).
|
||||
|
@ -1,8 +1,10 @@
|
||||
# balena CLI Installation Instructions for Linux
|
||||
|
||||
These instructions are for the recommended installation option. They are suitable for most Linux
|
||||
distributions, except notably for **Linux Alpine** or **Busybox**. For these distros, see [advanced
|
||||
installation options](./INSTALL-ADVANCED.md).
|
||||
These instructions are suitable for most Linux distributions on Intel x86, such as
|
||||
Ubuntu, Debian, Fedora, Arch Linux and other glibc-based distributions.
|
||||
For the ARM architecture and for Linux distributions not based on glibc, such as
|
||||
Alpine Linux, follow the [NPM Installation](./INSTALL-ADVANCED.md#npm-installation)
|
||||
method.
|
||||
|
||||
Selected operating system: **Linux**
|
||||
|
||||
@ -11,36 +13,53 @@ Selected operating system: **Linux**
|
||||
with "-standalone.zip", for example:
|
||||
`balena-cli-vX.Y.Z-linux-x64-standalone.zip`
|
||||
|
||||
2. Extract the zip file contents to any folder you choose. The extracted contents will include a
|
||||
`balena-cli` folder.
|
||||
2. Extract the zip file contents to any folder you choose, for example `/home/james`.
|
||||
The extracted contents will include a `balena-cli` folder.
|
||||
|
||||
3. Add the `balena-cli` folder to the system's `PATH` environment variable. There are several
|
||||
ways of achieving this on Linux: See this [StackOverflow post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix). Close and reopen the terminal window
|
||||
so that the changes to PATH can take effect.
|
||||
3. Add that folder (e.g. `/home/james/balena-cli`) to the `PATH` environment variable.
|
||||
Check this [StackOverflow
|
||||
post](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix)
|
||||
for instructions. Close and reopen the terminal window so that the changes to `PATH`
|
||||
can take effect.
|
||||
|
||||
4. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
terminal window:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy` and `preload` commands may require additional software to be installed, as described
|
||||
below.
|
||||
|
||||
To update the balena CLI to a new version, download a new release zip file and replace the previous
|
||||
installation folder. To uninstall, simply delete the folder and edit the PATH environment variable
|
||||
as described above.
|
||||
|
||||
## sudo configuration
|
||||
|
||||
A few CLI commands require execution through sudo, e.g. `sudo balena scan`.
|
||||
If your Linux distribution has an `/etc/sudoers` file that defines a `secure_path`
|
||||
setting, run `sudo visudo` to edit it and add the balena CLI's installation folder to
|
||||
the ***pre-existing*** `secure_path` setting, for example:
|
||||
|
||||
```text
|
||||
Defaults secure_path="/home/james/balena-cli:<pre-existing entries go here>"
|
||||
```
|
||||
|
||||
If an `/etc/sudoers` file does not exist, or if it does not contain a pre-existing
|
||||
`secure_path` setting, do not change it.
|
||||
|
||||
If you also have Docker installed, ensure that it can be executed ***without*** `sudo`, so that
|
||||
CLI commands like `balena build` and `balena preload` can also be executed without `sudo`.
|
||||
Check Docker's [post-installation
|
||||
steps](https://docs.docker.com/engine/install/linux-postinstall/) on how to achieve this.
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build, deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
|
@ -10,30 +10,36 @@ Selected operating system: **macOS**
|
||||
Look for a file name that ends with "-installer.pkg":
|
||||
`balena-cli-vX.Y.Z-macOS-x64-installer.pkg`
|
||||
|
||||
2. Double click the downloaded file to run the installer. 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).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- [Open the Terminal
|
||||
app](https://support.apple.com/en-gb/guide/terminal/apd5265185d-f365-44cb-8b09-71a064a42125/mac).
|
||||
- On the terminal prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `build`, `deploy`
|
||||
and `preload` commands may require additional software to be installed, as described below.
|
||||
and `preload` commands may require additional software to be installed, as described
|
||||
in the next section.
|
||||
|
||||
To update the balena CLI, repeat the steps above for the new version.
|
||||
To uninstall it, run the following command on a terminal prompt:
|
||||
|
||||
```text
|
||||
sudo /usr/local/lib/balena-cli/bin/uninstall
|
||||
```
|
||||
|
||||
## Additional Dependencies
|
||||
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -52,17 +58,17 @@ command set can also be used to list and manage SSH keys: see `balena help -v`.
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Mac](https://docs.docker.com/docker-for-mac/release-notes/#docker-community-edition-18061-ce-mac73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
@ -10,19 +10,17 @@ Selected operating system: **Windows**
|
||||
Look for a file name that ends with "-installer.exe":
|
||||
`balena-cli-vX.Y.Z-windows-x64-installer.exe`
|
||||
|
||||
2. Double click the downloaded file to run the installer. 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).
|
||||
2. Double click on the downloaded file to run the installer and follow the installer's
|
||||
instructions.
|
||||
|
||||
3. Check that the installation was successful by running the following commands on a
|
||||
command terminal:
|
||||
* `balena version` - should print the CLI's version
|
||||
* `balena help` - should print a list of available commands
|
||||
3. Check that the installation was successful:
|
||||
- Click on the Windows Start Menu, type PowerShell, and then click
|
||||
on Windows PowerShell.
|
||||
- On the command prompt, type `balena version` and hit Enter. It should display
|
||||
the version of the balena CLI that you have installed.
|
||||
|
||||
No further steps are required to run most CLI commands. The `balena ssh`, `scan`, `build`,
|
||||
`deploy`, `preload` and `os configure` commands may require additional software to be installed, as
|
||||
`deploy` and `preload` commands may require additional software to be installed, as
|
||||
described below.
|
||||
|
||||
## Additional Dependencies
|
||||
@ -30,11 +28,11 @@ described below.
|
||||
### build and deploy
|
||||
|
||||
These commands require [Docker](https://docs.docker.com/install/overview/) or
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available (on a local or remote machine). Most
|
||||
users will simply follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same laptop (dev
|
||||
machine) where the balena CLI is installed. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md) document describes other possibilities.
|
||||
[balenaEngine](https://www.balena.io/engine/) to be available on a local or remote
|
||||
machine. Most users will follow [Docker's installation
|
||||
instructions](https://docs.docker.com/install/overview/) to install Docker on the same
|
||||
workstation as the balena CLI. The [advanced installation
|
||||
options](./INSTALL-ADVANCED.md#additional-dependencies) document describes other possibilities.
|
||||
|
||||
### balena ssh
|
||||
|
||||
@ -59,24 +57,17 @@ Otherwise, Bonjour for Windows can be downloaded and installed from: https://sup
|
||||
|
||||
### balena preload
|
||||
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker, with the additional
|
||||
restriction that Docker must be installed on the local machine (because Docker's bind mounting
|
||||
feature is used). Also, for some device types (such as the Raspberry Pi), the `preload` command
|
||||
requires Docker to support the [AUFS storage
|
||||
Like the `build` and `deploy` commands, the `preload` command requires Docker.
|
||||
Preloading balenaOS images for some older device types (like the Raspberry
|
||||
Pi 3, but not the Raspberry 4) requires Docker to support the [AUFS storage
|
||||
driver](https://docs.docker.com/storage/storagedriver/aufs-driver/). Unfortunately, Docker Desktop
|
||||
for Windows dropped support for the AUFS filesystem in Docker CE versions greater than 18.06.1. The
|
||||
present workaround is to either:
|
||||
for Windows and macOS dropped support for the AUFS filesystem in Docker CE versions greater than
|
||||
18.06.1. The present workarounds are to either:
|
||||
|
||||
* Install the balena CLI on Linux (e.g. Ubuntu) with a virtual machine like VirtualBox.
|
||||
This works because Docker for Linux still supports AUFS. Hint: if using a virtual machine,
|
||||
copy the image file over, rather than accessing it through "file sharing", to avoid errors.
|
||||
* Downgrade Docker Desktop to version 18.06.1. Link: [Docker CE for
|
||||
Windows](https://docs.docker.com/docker-for-windows/release-notes/#docker-community-edition-18061-ce-win73-2018-08-29)
|
||||
* Install the balena CLI on a Linux machine (as Docker for Linux still supports AUFS). A Linux
|
||||
Virtual Machine also works, but a Docker container is _not_ recommended.
|
||||
|
||||
Long term, we are working on replacing AUFS with overlay2 for the affected device types.
|
||||
|
||||
### balena os configure
|
||||
|
||||
* The `balena os configure` command is currently not supported on Windows natively, but works with
|
||||
the [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about) (WSL). When
|
||||
using WSL, [install the balena CLI for
|
||||
Linux](https://github.com/balena-io/balena-cli/blob/master/INSTALL-LINUX.md).
|
||||
We are working on replacing AUFS with overlay2 in balenaOS images of the affected device types.
|
||||
|
31
README.md
31
README.md
@ -28,18 +28,20 @@ are supported. Alternative shells include:
|
||||
|
||||
* [MSYS2](https://www.msys2.org/):
|
||||
* Install additional packages with the command:
|
||||
`pacman -S git openssh rsync`
|
||||
`pacman -S git gcc make openssh p7zip`
|
||||
* [Set a Windows environment variable](https://www.onmsft.com/how-to/how-to-set-an-environment-variable-in-windows-10): `MSYS2_PATH_TYPE=inherit`
|
||||
* Note that a bug in the MSYS2 launch script (`msys2_shell.cmd`) makes text-based interactive CLI
|
||||
menus to break. [Check this Github issue for a
|
||||
workaround](https://github.com/msys2/MINGW-packages/issues/1633#issuecomment-240583890).
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS): select the `msys-rsync` and `msys-openssh` packages too
|
||||
|
||||
* [MSYS](http://www.mingw.org/wiki/MSYS)
|
||||
* [Git for Windows](https://git-for-windows.github.io/)
|
||||
* During the installation, you will be prompted to choose between _"Use MinTTY"_ and _"Use
|
||||
Windows' default console window"._ Choose the latter, because of the same [MSYS2
|
||||
bug](https://github.com/msys2/MINGW-packages/issues/1633) mentioned above (Git for Windows
|
||||
actually uses MSYS2). For a screenshot, check this
|
||||
[comment](https://github.com/balena-io/balena-cli/issues/598#issuecomment-556513098).
|
||||
|
||||
* Microsoft's [Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/about)
|
||||
(WSL). In this case, a Linux distribution like Ubuntu is installed via the Microsoft Store, and a
|
||||
balena CLI release **for Linux** should be selected. See
|
||||
@ -48,14 +50,14 @@ are supported. Alternative shells include:
|
||||
|
||||
On **macOS** and **Linux,** the standard terminal window is supported. Optionally, `bash` command
|
||||
auto completion may be enabled by copying the
|
||||
[balena-completion.bash](https://github.com/balena-io/balena-cli/blob/master/balena-completion.bash)
|
||||
[balena_comp](https://github.com/balena-io/balena-cli/blob/master/completion/balena-completion.bash)
|
||||
file to your system's `bash_completion` directory: check [Docker's command completion
|
||||
guide](https://docs.docker.com/compose/completion/) for system setup instructions.
|
||||
|
||||
## Logging in
|
||||
|
||||
Several CLI commands require access to your balenaCloud account, for example in order to push a
|
||||
new release to your application. Those commands require creating a CLI login session by running:
|
||||
new release to your fleet. Those commands require creating a CLI login session by running:
|
||||
|
||||
```sh
|
||||
$ balena login
|
||||
@ -75,7 +77,6 @@ HTTP(S) proxies can be configured through any of the following methods, in prece
|
||||
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'
|
||||
@ -143,7 +144,7 @@ To learn more, troubleshoot issues, or to contact us for support:
|
||||
|
||||
* Check the [masterclass tutorials](https://www.balena.io/docs/learn/more/masterclasses/overview/)
|
||||
* Check our [FAQ / troubleshooting document](https://github.com/balena-io/balena-cli/blob/master/TROUBLESHOOTING.md)
|
||||
* Ask us a question through the [balenaCloud forum](https://forums.balena.io/c/balena-cloud)
|
||||
* Ask us a question in the [balena forums](https://forums.balena.io/c/product-support)
|
||||
|
||||
For CLI bug reports or feature requests, check the
|
||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||
@ -155,14 +156,18 @@ of major, minor and patch version releases.
|
||||
|
||||
The latest release of a major version of the balena CLI will remain compatible with
|
||||
the balenaCloud backend services for at least one year from the date when the
|
||||
following major version is released. For example, balena CLI 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.
|
||||
following major version is released. For example, balena CLI v11.36.0, as the
|
||||
latest v11 release, would remain compatible with the balenaCloud backend for one
|
||||
year from the date when v12.0.0 was released.
|
||||
|
||||
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.
|
||||
Half way through to that period (6 months after the release of the next major
|
||||
version), older major versions of the balena CLI will start printing a deprecation
|
||||
warning message when it is used interactively (when `stderr` is attached to a TTY
|
||||
device file). At the end of that period, older major versions will exit with an
|
||||
error message unless the `--unsupported` flag is used. This behavior was
|
||||
introduced in CLI version 12.47.0 and is also documented by `balena help`.
|
||||
To take advantage of the latest backend features and ensure compatibility, users
|
||||
are encouraged to regularly update the balena CLI to the latest version.
|
||||
|
||||
## Contributing (including editing documentation files)
|
||||
|
||||
|
@ -31,6 +31,11 @@ command again.
|
||||
|
||||
Check whether the SD card is locked (a physical switch on the side of the card).
|
||||
|
||||
## I get `connect ETIMEDOUT` with `balena tunnel`
|
||||
|
||||
Please update the CLI to the latest version. This issue was fixed in v12.38.5.
|
||||
For more details, see: https://github.com/balena-io/balena-cli/issues/2172
|
||||
|
||||
## I get EINVAL errors on Cygwin
|
||||
|
||||
The errors may look something like this:
|
||||
|
43
appveyor.yml
43
appveyor.yml
@ -1,43 +0,0 @@
|
||||
# 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')
|
@ -15,31 +15,33 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { JsonVersions } from '../lib/commands/version';
|
||||
import type { JsonVersions } from '../lib/commands/version/index';
|
||||
|
||||
import { run as oclifRun } from '@oclif/dev-cli';
|
||||
import { run as oclifRun } from '@oclif/core';
|
||||
import * as archiver from 'archiver';
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { execFile } from 'child_process';
|
||||
import * as filehound from 'filehound';
|
||||
import { Stats } from 'fs';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as _ from 'lodash';
|
||||
import * as klaw from 'klaw';
|
||||
import * as path from 'path';
|
||||
import * as rimraf from 'rimraf';
|
||||
import * as semver from 'semver';
|
||||
import * as util from 'util';
|
||||
import { promisify } from 'util';
|
||||
import { notarize } from '@electron/notarize';
|
||||
|
||||
import { stripIndent } from '../lib/utils/lazy';
|
||||
import { stripIndent } from '../build/utils/lazy';
|
||||
import {
|
||||
diffLines,
|
||||
getSubprocessStdout,
|
||||
loadPackageJson,
|
||||
MSYS2_BASH,
|
||||
ROOT,
|
||||
StdOutTap,
|
||||
whichSpawn,
|
||||
} from './utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const packageJSON = loadPackageJson();
|
||||
export const version = 'v' + packageJSON.version;
|
||||
const arch = process.arch;
|
||||
@ -60,7 +62,7 @@ const standaloneZips: PathByPlatform = {
|
||||
|
||||
const oclifInstallers: PathByPlatform = {
|
||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
||||
win32: dPath('win32', `balena-${version}-${arch}.exe`),
|
||||
};
|
||||
|
||||
const renamedOclifInstallers: PathByPlatform = {
|
||||
@ -85,13 +87,14 @@ async function diffPkgOutput(pkgOut: string) {
|
||||
'tests',
|
||||
'test-data',
|
||||
'pkg',
|
||||
`expected-warnings-${process.platform}.txt`,
|
||||
`expected-warnings-${process.platform}-${arch}.txt`,
|
||||
);
|
||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||
const ignoreStartsWith = [
|
||||
'> pkg@',
|
||||
'> Fetching base Node.js binaries',
|
||||
' fetched-',
|
||||
'prebuild-install WARN install No prebuilt binaries found',
|
||||
];
|
||||
const modulesRE =
|
||||
process.platform === 'win32'
|
||||
@ -152,7 +155,7 @@ ${sep}
|
||||
* messages (stdout and stderr) in order to call diffPkgOutput().
|
||||
*/
|
||||
async function execPkg(...args: any[]) {
|
||||
const { exec: pkgExec } = await import('pkg');
|
||||
const { exec: pkgExec } = await import('@yao-pkg/pkg');
|
||||
const outTap = new StdOutTap(true);
|
||||
try {
|
||||
outTap.tap();
|
||||
@ -177,9 +180,18 @@ async function execPkg(...args: any[]) {
|
||||
* to be directly executed from inside another binary executable.)
|
||||
*/
|
||||
async function buildPkg() {
|
||||
// https://github.com/vercel/pkg#targets
|
||||
let targets = `linux-${arch}`;
|
||||
// TBC: not possible to build for macOS or Windows arm64 on x64 nodes
|
||||
if (process.platform === 'darwin') {
|
||||
targets = `macos-x64`;
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
targets = `win-x64`;
|
||||
}
|
||||
const args = [
|
||||
'--target',
|
||||
'host',
|
||||
'--targets',
|
||||
targets,
|
||||
'--output',
|
||||
'build-bin/balena',
|
||||
'package.json',
|
||||
@ -194,7 +206,6 @@ async function buildPkg() {
|
||||
const paths: Array<[string, string[], string[]]> = [
|
||||
// [platform, [source path], [destination path]]
|
||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||
];
|
||||
await Promise.all(
|
||||
@ -243,7 +254,17 @@ async function testPkg() {
|
||||
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 { stdout, stderr } = await execFileAsync(pkgBalenaPath, [
|
||||
'version',
|
||||
'-j',
|
||||
]);
|
||||
const { filterCliOutputForTests } = await import('../tests/helpers');
|
||||
const filtered = filterCliOutputForTests({
|
||||
err: stderr.split(/\r?\n/),
|
||||
out: stdout.split(/\r?\n/),
|
||||
});
|
||||
stdout = filtered.out.join('\n');
|
||||
stderr = filtered.err.join('\n');
|
||||
let pkgNodeVersion = '';
|
||||
let pkgNodeMajorVersion = 0;
|
||||
try {
|
||||
@ -260,6 +281,10 @@ async function testPkg() {
|
||||
`Mismatched major version: built-in pkg Node version="${pkgNodeVersion}" vs process.version="${process.version}"`,
|
||||
);
|
||||
}
|
||||
if (filtered.err.length > 0) {
|
||||
const err = filtered.err.join('\n');
|
||||
throw new Error(`"${pkgBalenaPath}": non-empty stderr "${err}"`);
|
||||
}
|
||||
console.log('Success! (standalone package test successful)');
|
||||
}
|
||||
|
||||
@ -292,10 +317,96 @@ async function zipPkg() {
|
||||
archive.on('warning', console.warn);
|
||||
|
||||
archive.pipe(outputStream);
|
||||
archive.finalize();
|
||||
archive.finalize().catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function signFilesForNotarization() {
|
||||
console.log('Deleting unneeded zip files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node.bak')) {
|
||||
console.log('Removing pkg .node.bak file', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
if (
|
||||
path.basename(item.path).endsWith('.zip') &&
|
||||
path.dirname(item.path).includes('test')
|
||||
) {
|
||||
console.log('Removing zip', item.path);
|
||||
fs.unlinkSync(item.path);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
// Sign all .node files first
|
||||
console.log('Signing .node files...');
|
||||
await new Promise((resolve, reject) => {
|
||||
klaw('node_modules/')
|
||||
.on('data', async (item: { path: string; stats: Stats }) => {
|
||||
if (!item.stats.isFile()) {
|
||||
return;
|
||||
}
|
||||
if (path.basename(item.path).endsWith('.node')) {
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
item.path,
|
||||
]);
|
||||
}
|
||||
})
|
||||
.on('end', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
console.log('Signing other binaries...');
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/denymount/bin/denymount',
|
||||
]);
|
||||
console.log('running command:', 'codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
await whichSpawn('codesign', [
|
||||
'-d',
|
||||
'-f',
|
||||
'--options=runtime',
|
||||
'-s',
|
||||
'Developer ID Application: Balena Ltd (66H43P8FRG)',
|
||||
'node_modules/macmount/bin/macmount',
|
||||
]);
|
||||
}
|
||||
|
||||
export async function buildStandaloneZip() {
|
||||
console.log(`Building standalone zip package for CLI ${version}`);
|
||||
try {
|
||||
@ -320,22 +431,28 @@ async function renameInstallerFiles() {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||
*/
|
||||
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',
|
||||
// trust ...
|
||||
await execFileAsync('signtool.exe', [
|
||||
'sign',
|
||||
'-t',
|
||||
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||
'-f',
|
||||
exeName,
|
||||
process.env.CSC_LINK,
|
||||
'-p',
|
||||
process.env.CSC_KEY_PASSWORD,
|
||||
'-d',
|
||||
`balena-cli ${version}`,
|
||||
exeName,
|
||||
]);
|
||||
// ... but verify
|
||||
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
||||
} else {
|
||||
console.log(
|
||||
'Skipping installer signing step because CSC_* env vars are not set',
|
||||
@ -344,7 +461,27 @@ async function signWindowsInstaller() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
||||
* Wait for Apple Installer Notarization to continue
|
||||
*/
|
||||
async function notarizeMacInstaller(): Promise<void> {
|
||||
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
|
||||
const appleId =
|
||||
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||
|
||||
if (appleIdPassword && teamId) {
|
||||
await notarize({
|
||||
tool: 'notarytool',
|
||||
teamId,
|
||||
appPath: renamedOclifInstallers.darwin,
|
||||
appleId,
|
||||
appleIdPassword,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the `oclif pack:win` or `pack:macos` command (depending on the value
|
||||
* of process.platform) to generate the native installers (which end up under
|
||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||
* 64-bit binaries under Windows.
|
||||
@ -354,9 +491,10 @@ export async function buildOclifInstaller() {
|
||||
let packOpts = ['-r', ROOT];
|
||||
if (process.platform === 'darwin') {
|
||||
packOS = 'macos';
|
||||
packOpts = packOpts.concat('--targets', 'darwin-x64');
|
||||
} else if (process.platform === 'win32') {
|
||||
packOS = 'win';
|
||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
||||
packOpts = packOpts.concat('--targets', 'win32-x64');
|
||||
}
|
||||
if (packOS) {
|
||||
console.log(`Building oclif installer for CLI ${version}`);
|
||||
@ -369,11 +507,16 @@ export async function buildOclifInstaller() {
|
||||
console.log(`rimraf(${dir})`);
|
||||
await Bluebird.fromCallback((cb) => rimraf(dir, cb));
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
console.log('Signing files for notarization...');
|
||||
await signFilesForNotarization();
|
||||
}
|
||||
console.log('=======================================================');
|
||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
||||
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||
console.log('=======================================================');
|
||||
await oclifRun([packCmd].concat(...packOpts));
|
||||
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||
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
|
||||
@ -381,6 +524,10 @@ export async function buildOclifInstaller() {
|
||||
// (`oclif.macos.sign` section).
|
||||
if (process.platform === 'win32') {
|
||||
await signWindowsInstaller();
|
||||
} else if (process.platform === 'darwin') {
|
||||
console.log('Notarizing package...');
|
||||
await notarizeMacInstaller(); // Notarize
|
||||
console.log('Package notarized.');
|
||||
}
|
||||
console.log(`oclif installer build completed`);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
import * as path from 'path';
|
||||
import { MarkdownFileParser } from './utils';
|
||||
import { GlobSync } from 'glob';
|
||||
|
||||
/**
|
||||
* This is the skeleton of CLI documentation/reference web page at:
|
||||
@ -24,163 +25,114 @@ import { MarkdownFileParser } from './utils';
|
||||
*
|
||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||
* some content to this object.
|
||||
*
|
||||
* IMPORTANT
|
||||
*
|
||||
* All commands need to be stored under a folder in lib/commands to maintain uniformity
|
||||
* Generating docs will error out if directive not followed
|
||||
* To add a custom heading for command docs, add the heading next to the folder name
|
||||
* in the `commandHeadings` dictionary.
|
||||
*
|
||||
* This dictionary is the source of truth that creates the docs config which is used
|
||||
* to generate the CLI documentation. By default, the folder name will be used.
|
||||
*
|
||||
* Resources with plural names needs to have 2 sections if they have commands like:
|
||||
* "fleet, fleets" or "device, devices" or "tag, tags"
|
||||
*
|
||||
*/
|
||||
const capitanoDoc = {
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [
|
||||
{
|
||||
title: 'API keys',
|
||||
files: ['build/commands/api-key/generate.js'],
|
||||
},
|
||||
{
|
||||
title: 'Application',
|
||||
files: [
|
||||
'build/commands/apps.js',
|
||||
'build/commands/app/index.js',
|
||||
'build/commands/app/create.js',
|
||||
'build/commands/app/purge.js',
|
||||
'build/commands/app/rename.js',
|
||||
'build/commands/app/restart.js',
|
||||
'build/commands/app/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Authentication',
|
||||
files: [
|
||||
'build/commands/login.js',
|
||||
'build/commands/logout.js',
|
||||
'build/commands/whoami.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Device',
|
||||
files: [
|
||||
'build/commands/devices/index.js',
|
||||
'build/commands/devices/supported.js',
|
||||
'build/commands/device/index.js',
|
||||
'build/commands/device/identify.js',
|
||||
'build/commands/device/init.js',
|
||||
'build/commands/device/move.js',
|
||||
'build/commands/device/os-update.js',
|
||||
'build/commands/device/public-url.js',
|
||||
'build/commands/device/purge.js',
|
||||
'build/commands/device/reboot.js',
|
||||
'build/commands/device/register.js',
|
||||
'build/commands/device/rename.js',
|
||||
'build/commands/device/restart.js',
|
||||
'build/commands/device/rm.js',
|
||||
'build/commands/device/shutdown.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Environment Variables',
|
||||
files: [
|
||||
'build/commands/envs.js',
|
||||
'build/commands/env/add.js',
|
||||
'build/commands/env/rename.js',
|
||||
'build/commands/env/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
files: [
|
||||
'build/commands/tags.js',
|
||||
'build/commands/tag/rm.js',
|
||||
'build/commands/tag/set.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help and Version',
|
||||
files: ['help', 'build/commands/version.js'],
|
||||
},
|
||||
{
|
||||
title: 'Keys',
|
||||
files: [
|
||||
'build/commands/keys.js',
|
||||
'build/commands/key/index.js',
|
||||
'build/commands/key/add.js',
|
||||
'build/commands/key/rm.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Logs',
|
||||
files: ['build/commands/logs.js'],
|
||||
},
|
||||
{
|
||||
title: 'Network',
|
||||
files: [
|
||||
'build/commands/scan.js',
|
||||
'build/commands/ssh.js',
|
||||
'build/commands/tunnel.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
files: ['build/commands/note.js'],
|
||||
},
|
||||
{
|
||||
title: 'OS',
|
||||
files: [
|
||||
'build/commands/os/build-config.js',
|
||||
'build/commands/os/configure.js',
|
||||
'build/commands/os/versions.js',
|
||||
'build/commands/os/download.js',
|
||||
'build/commands/os/initialize.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Config',
|
||||
files: [
|
||||
'build/commands/config/generate.js',
|
||||
'build/commands/config/inject.js',
|
||||
'build/commands/config/read.js',
|
||||
'build/commands/config/reconfigure.js',
|
||||
'build/commands/config/write.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Preload',
|
||||
files: ['build/commands/preload.js'],
|
||||
},
|
||||
{
|
||||
title: 'Push',
|
||||
files: ['build/commands/push.js'],
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
files: ['build/commands/settings.js'],
|
||||
},
|
||||
{
|
||||
title: 'Local',
|
||||
files: [
|
||||
'build/commands/local/configure.js',
|
||||
'build/commands/local/flash.js',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Deploy',
|
||||
files: ['build/commands/build.js', 'build/commands/deploy.js'],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
files: ['build/commands/join.js', 'build/commands/leave.js'],
|
||||
},
|
||||
{
|
||||
title: 'Utilities',
|
||||
files: ['build/commands/util/available-drives.js'],
|
||||
},
|
||||
{
|
||||
title: 'Support',
|
||||
files: ['build/commands/support.js'],
|
||||
},
|
||||
],
|
||||
|
||||
interface Category {
|
||||
title: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
interface Documentation {
|
||||
title: string;
|
||||
introduction: string;
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
// Mapping folders names to custom headings in the docs
|
||||
const commandHeadings: { [key: string]: string } = {
|
||||
'api-key': 'API Key',
|
||||
'api-keys': 'API Keys',
|
||||
login: 'Authentication',
|
||||
whoami: 'Authentication',
|
||||
logout: 'Authentication',
|
||||
env: 'Environment Variable',
|
||||
envs: 'Environment Variables',
|
||||
help: 'Help and Version',
|
||||
key: 'SSH Key',
|
||||
keys: 'SSH Keys',
|
||||
orgs: 'Organizations',
|
||||
os: 'OS',
|
||||
util: 'Utilities',
|
||||
ssh: 'Network',
|
||||
scan: 'Network',
|
||||
tunnel: 'Network',
|
||||
build: 'Deploy',
|
||||
join: 'Platform',
|
||||
leave: 'Platform',
|
||||
};
|
||||
|
||||
// Fetch all available commands
|
||||
const allCommandsPaths = new GlobSync('build/commands/**/*.js', {
|
||||
ignore: 'build/commands/internal/**',
|
||||
}).found;
|
||||
|
||||
// Throw error if any commands found outside of command directories
|
||||
const illegalCommandPaths = allCommandsPaths.filter((commandPath: string) =>
|
||||
/^build\/commands\/[^/]+\.js$/.test(commandPath),
|
||||
);
|
||||
|
||||
if (illegalCommandPaths.length !== 0) {
|
||||
throw new Error(
|
||||
`Found the following commands without a command directory: ${illegalCommandPaths}\n
|
||||
To resolve this error, move the respective commands to their resource directories or create new ones.\n
|
||||
Refer to the automation/capitanodoc/capitanodoc.ts file for more information.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Docs config template
|
||||
const capitanoDoc: Documentation = {
|
||||
title: 'balena CLI Documentation',
|
||||
introduction: '',
|
||||
categories: [],
|
||||
};
|
||||
|
||||
// Helper function to capitalize each word of directory name
|
||||
function formatTitle(dir: string): string {
|
||||
return dir.replace(/(^\w|\s\w)/g, (word) => word.toUpperCase());
|
||||
}
|
||||
|
||||
// Create a map to track the categories for faster lookup
|
||||
const categoriesMap: { [key: string]: Category } = {};
|
||||
|
||||
for (const commandPath of allCommandsPaths) {
|
||||
const commandDir = path.basename(path.dirname(commandPath));
|
||||
const heading = commandHeadings[commandDir] || formatTitle(commandDir);
|
||||
|
||||
if (!categoriesMap[heading]) {
|
||||
categoriesMap[heading] = { title: heading, files: [] };
|
||||
capitanoDoc.categories.push(categoriesMap[heading]);
|
||||
}
|
||||
|
||||
categoriesMap[heading].files.push(commandPath);
|
||||
}
|
||||
|
||||
// Sort Category titles alphabetically
|
||||
capitanoDoc.categories = capitanoDoc.categories.sort((a, b) =>
|
||||
a.title.localeCompare(b.title),
|
||||
);
|
||||
|
||||
// Sort Category file paths alphabetically
|
||||
capitanoDoc.categories.forEach((category) => {
|
||||
category.files.sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
/**
|
||||
* 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/
|
||||
* Modify and return the `capitanoDoc` object above in order to generate the
|
||||
* CLI documentation at docs/balena-cli.md
|
||||
*
|
||||
* This function parses the README.md file to extract relevant sections
|
||||
* for the documentation web page.
|
||||
|
2
automation/capitanodoc/doc-types.d.ts
vendored
2
automation/capitanodoc/doc-types.d.ts
vendored
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Command as OclifCommandClass } from '@oclif/command';
|
||||
import { Command as OclifCommandClass } from '@oclif/core';
|
||||
|
||||
type OclifCommand = typeof OclifCommandClass;
|
||||
|
||||
|
@ -58,16 +58,15 @@ class FakeHelpCommand {
|
||||
|
||||
examples = [
|
||||
'$ balena help',
|
||||
'$ balena help apps',
|
||||
'$ balena help login',
|
||||
'$ balena help os download',
|
||||
];
|
||||
|
||||
args = [
|
||||
{
|
||||
name: 'command',
|
||||
args = {
|
||||
command: {
|
||||
description: 'command to show help for',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
usage = 'help [command]';
|
||||
|
||||
@ -86,7 +85,7 @@ function importOclifCommands(jsFilename: string): OclifCommand[] {
|
||||
|
||||
const command: OclifCommand =
|
||||
jsFilename === 'help'
|
||||
? ((new FakeHelpCommand() as unknown) as OclifCommand)
|
||||
? (new FakeHelpCommand() as unknown as OclifCommand)
|
||||
: (require(path.join(process.cwd(), jsFilename)).default as OclifCommand);
|
||||
|
||||
return [command];
|
||||
@ -101,8 +100,9 @@ async function printMarkdown() {
|
||||
console.log(await renderMarkdown());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
printMarkdown();
|
||||
|
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flagUsages } from '@oclif/parser';
|
||||
import { Parser } from '@oclif/core';
|
||||
import * as ent from 'ent';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
@ -37,8 +37,8 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
|
||||
if (!_.isEmpty(command.args)) {
|
||||
result.push('### Arguments');
|
||||
for (const arg of command.args!) {
|
||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
||||
for (const [name, arg] of Object.entries(command.args!)) {
|
||||
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
||||
continue;
|
||||
}
|
||||
flag.name = name;
|
||||
const flagUsage = flagUsages([flag])
|
||||
const flagUsage = Parser.flagUsages([flag])
|
||||
.map(([usage, _description]) => usage)
|
||||
.join()
|
||||
.trim();
|
||||
|
@ -15,24 +15,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const stripIndent = require('common-tags/lib/stripIndent');
|
||||
const _ = require('lodash');
|
||||
const { promises: fs } = require('fs');
|
||||
const path = require('path');
|
||||
const simplegit = require('simple-git/promise');
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { simpleGit } from 'simple-git';
|
||||
|
||||
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
|
||||
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
|
||||
* issuing an error if balena-cli.md is older.
|
||||
* If balena-cli.md does not require updating and the developer cannot run
|
||||
* `npm run build` on their laptop, the error message suggests a workaround
|
||||
* using `touch`.
|
||||
*/
|
||||
async function checkBuildTimestamps() {
|
||||
const git = simplegit(ROOT);
|
||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
||||
const git = simpleGit(ROOT);
|
||||
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||
const [docStat, gitStatus] = await Promise.all([
|
||||
fs.stat(docFile),
|
||||
git.status(),
|
||||
@ -81,4 +82,5 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
@ -6,6 +6,8 @@
|
||||
*
|
||||
* We don't `require('semver')` to allow this script to be run as a npm
|
||||
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||
*
|
||||
* @param {string} version
|
||||
*/
|
||||
function parseSemver(version) {
|
||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(version);
|
||||
@ -16,9 +18,13 @@ function parseSemver(version) {
|
||||
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} v1
|
||||
* @param {string} v2
|
||||
*/
|
||||
function semverGte(v1, v2) {
|
||||
let v1Array = parseSemver(v1);
|
||||
let v2Array = parseSemver(v2);
|
||||
const v1Array = parseSemver(v1);
|
||||
const v2Array = parseSemver(v2);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (v1Array[i] < v2Array[i]) {
|
||||
return false;
|
||||
@ -41,17 +47,25 @@ function checkNpmVersion() {
|
||||
// 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(`\
|
||||
-------------------------------------------------------------------------------
|
||||
throw new Error(`\
|
||||
-----------------------------------------------------------------------------
|
||||
Error: npm version '${npmVersion}' detected. Please upgrade to npm v${requiredVersion} or later
|
||||
because of a bug that causes the 'npm-shrinkwrap.json' file to be damaged.
|
||||
At this point, however, your 'npm-shrinkwrap.json' file has already been
|
||||
damaged. Please revert it to the master branch state with a command such as:
|
||||
"git checkout master -- npm-shrinkwrap.json"
|
||||
Then re-run "npm install" using npm version ${requiredVersion} or later.
|
||||
-------------------------------------------------------------------------------`);
|
||||
process.exit(1);
|
||||
-----------------------------------------------------------------------------`);
|
||||
}
|
||||
}
|
||||
|
||||
checkNpmVersion();
|
||||
function main() {
|
||||
try {
|
||||
checkNpmVersion();
|
||||
} catch (e) {
|
||||
console.error(e.message || e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -30,7 +30,7 @@ const { GITHUB_TOKEN } = process.env;
|
||||
export async function createGitHubRelease() {
|
||||
console.log(`Publishing release ${version} to GitHub`);
|
||||
const publishRelease = await import('publish-release');
|
||||
const ghRelease = await Bluebird.fromCallback(
|
||||
const ghRelease = (await Bluebird.fromCallback(
|
||||
publishRelease.bind(null, {
|
||||
token: GITHUB_TOKEN || '',
|
||||
owner: 'balena-io',
|
||||
@ -40,7 +40,7 @@ export async function createGitHubRelease() {
|
||||
reuseRelease: true,
|
||||
assets: finalReleaseAssets[process.platform],
|
||||
}),
|
||||
);
|
||||
)) as { html_url: any };
|
||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||
}
|
||||
|
||||
@ -54,17 +54,18 @@ export async function release() {
|
||||
try {
|
||||
await createGitHubRelease();
|
||||
} catch (err) {
|
||||
console.error('Release failed');
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
throw new Error(`Error creating GitHub release:\n${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a cached Octokit instance, creating a new one as needed. */
|
||||
const getOctokit = _.once(function () {
|
||||
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
|
||||
(require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling'))
|
||||
.throttling,
|
||||
const Octokit = (
|
||||
require('@octokit/rest') as typeof import('@octokit/rest')
|
||||
).Octokit.plugin(
|
||||
(
|
||||
require('@octokit/plugin-throttling') as typeof import('@octokit/plugin-throttling')
|
||||
).throttling,
|
||||
);
|
||||
return new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
@ -110,7 +111,8 @@ function getPageNumbers(
|
||||
if (!response.headers.link) {
|
||||
return res;
|
||||
}
|
||||
const parse = require('parse-link-header') as typeof import('parse-link-header');
|
||||
const parse =
|
||||
require('parse-link-header') as typeof import('parse-link-header');
|
||||
const parsed = parse(response.headers.link);
|
||||
if (parsed == null) {
|
||||
throw new Error(`Failed to parse link header: '${response.headers.link}'`);
|
||||
@ -152,17 +154,20 @@ async function updateGitHubReleaseDescriptions(
|
||||
) {
|
||||
const perPage = 30;
|
||||
const octokit = getOctokit();
|
||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
||||
const options = 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,
|
||||
);
|
||||
type Release =
|
||||
import('@octokit/rest').RestEndpointMethodTypes['repos']['listReleases']['response']['data'][0];
|
||||
for await (const response of octokit.paginate.iterator<Release>(options)) {
|
||||
const {
|
||||
page: thisPage,
|
||||
pages: totalPages,
|
||||
ordinal,
|
||||
} = getPageNumbers(response, perPage);
|
||||
let i = 0;
|
||||
for (const cliRelease of response.data) {
|
||||
const prefix = `[#${ordinal + i++} pg ${thisPage}/${totalPages}]`;
|
||||
|
@ -27,7 +27,6 @@ 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(
|
||||
@ -36,11 +35,6 @@ process.env.DEBUG = ['0', 'no', 'false', '', undefined].includes(
|
||||
? ''
|
||||
: '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:
|
||||
@ -48,17 +42,14 @@ function exitWithError(error: Error | string): never {
|
||||
* '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[]) {
|
||||
async function parse(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}]`);
|
||||
console.error(`[debug] automation/run.ts process.argv=[${process.argv}]`);
|
||||
console.error(`[debug] automation/run.ts args=[${args}]`);
|
||||
if (_.isEmpty(args)) {
|
||||
return exitWithError('missing command-line arguments');
|
||||
throw new Error('missing command-line arguments');
|
||||
}
|
||||
const commands: { [cmd: string]: () => void | Promise<void> } = {
|
||||
'build:installer': buildOclifInstaller,
|
||||
@ -69,15 +60,11 @@ export async function run(args?: string[]) {
|
||||
release,
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (!commands.hasOwnProperty(arg)) {
|
||||
return exitWithError(`command unknown: ${arg}`);
|
||||
if (!Object.hasOwn(commands, arg)) {
|
||||
throw new Error(`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
|
||||
@ -95,29 +82,26 @@ export async function run(args?: string[]) {
|
||||
|
||||
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}`);
|
||||
if (typeof err === 'object') {
|
||||
err.message = `"${arg}": ${err.message}`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** See jsdoc for parse() function above */
|
||||
export async function run(args?: string[]) {
|
||||
try {
|
||||
await parse(args);
|
||||
} catch (e) {
|
||||
console.error(e.message ? `Error: ${e.message}` : e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
||||
|
@ -10,7 +10,10 @@ npm i
|
||||
if ! diff -q npm-shrinkwrap.json npm-shrinkwrap.json.old > /dev/null; then
|
||||
rm npm-shrinkwrap.json.old
|
||||
echo "** npm-shrinkwrap.json was not deduplicated or not fully committed - FAIL **";
|
||||
echo "** Please run 'npm ci', followed by 'npm dedupe' **";
|
||||
echo "** This can usually be fixed with: **";
|
||||
echo "** git checkout master -- npm-shrinkwrap.json **";
|
||||
echo "** rm -rf node_modules **";
|
||||
echo "** npm install && npm dedupe && npm install **";
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
|
@ -11,8 +11,7 @@ const validateChangeType = (maybeChangeType: string = 'minor') => {
|
||||
case 'major':
|
||||
return maybeChangeType;
|
||||
default:
|
||||
console.error(`Invalid change type: '${maybeChangeType}'`);
|
||||
return process.exit(1);
|
||||
throw new Error(`Invalid change type: '${maybeChangeType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -37,8 +36,8 @@ const run = async (cmd: string) => {
|
||||
}
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
p.stdout.pipe(process.stdout);
|
||||
p.stderr.pipe(process.stderr);
|
||||
p.stdout?.pipe(process.stdout);
|
||||
p.stderr?.pipe(process.stderr);
|
||||
});
|
||||
};
|
||||
|
||||
@ -58,31 +57,24 @@ const getUpstreams = async () => {
|
||||
const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');
|
||||
|
||||
const yaml = await import('js-yaml');
|
||||
const { upstream } = yaml.safeLoad(repoYaml) as {
|
||||
const { upstream } = yaml.load(repoYaml) as {
|
||||
upstream: Upstream[];
|
||||
};
|
||||
|
||||
return upstream;
|
||||
};
|
||||
|
||||
const printUsage = (upstreams: Upstream[], upstreamName: string) => {
|
||||
console.error(
|
||||
`
|
||||
const getUsage = (upstreams: Upstream[], upstreamName: string) => `
|
||||
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() {
|
||||
async function $main() {
|
||||
const upstreams = await getUpstreams();
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
return printUsage(upstreams, '$upstreamName');
|
||||
throw new Error(getUsage(upstreams, '$upstreamName'));
|
||||
}
|
||||
|
||||
const upstreamName = process.argv[2];
|
||||
@ -90,16 +82,15 @@ async function main() {
|
||||
const upstream = upstreams.find((v) => v.repo === upstreamName);
|
||||
|
||||
if (!upstream) {
|
||||
console.error(
|
||||
throw new Error(
|
||||
`Invalid upstream name '${upstreamName}', valid options: ${upstreams
|
||||
.map(({ repo }) => repo)
|
||||
.join(', ')}`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
printUsage(upstreams, upstreamName);
|
||||
throw new Error(getUsage(upstreams, upstreamName));
|
||||
}
|
||||
|
||||
const packageName = upstream.module || upstream.repo;
|
||||
@ -108,8 +99,7 @@ async function main() {
|
||||
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);
|
||||
throw new Error(`Already on version '${newVersion}'`);
|
||||
}
|
||||
|
||||
console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
|
||||
@ -137,4 +127,14 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await $main();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
main();
|
||||
|
@ -16,12 +16,8 @@
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as shellEscape from 'shell-escape';
|
||||
|
||||
export const MSYS2_BASH =
|
||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
||||
export const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/** Tap and buffer this process' stdout and stderr */
|
||||
@ -91,93 +87,6 @@ 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
|
||||
@ -207,7 +116,7 @@ export async function which(program: string): Promise<string> {
|
||||
*/
|
||||
export async function whichSpawn(
|
||||
programName: string,
|
||||
args?: string[],
|
||||
args: string[] = [],
|
||||
): Promise<void> {
|
||||
const program = await which(programName);
|
||||
let error: Error | undefined;
|
||||
|
@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
commands="app apps build config deploy device devices env envs help key \
|
||||
keys local login logout logs note os preload quickstart settings \
|
||||
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
|
21
bin/balena
21
bin/balena
@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
@ -9,14 +7,15 @@ 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',
|
||||
});
|
||||
async function run() {
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
await require('../build/fast-boot').start();
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
// Set the desired es version for downstream modules that support it
|
||||
require('@balena/es-version').set('es2018');
|
||||
|
||||
// Run the CLI
|
||||
require('../build/app').run();
|
||||
// Run the CLI
|
||||
await require('../build/app').run(undefined, { dir: __dirname });
|
||||
}
|
||||
|
||||
run();
|
||||
|
@ -5,12 +5,26 @@
|
||||
// Before opening a PR you should build and test your changes using bin/balena
|
||||
// ****************************************************************************
|
||||
|
||||
// tslint:disable:no-var-requires
|
||||
|
||||
// We boost the threadpool size as ext2fs can deadlock with some
|
||||
// operations otherwise, if the pool runs out.
|
||||
process.env.UV_THREADPOOL_SIZE = '64';
|
||||
|
||||
// Note on `fast-boot2`: We do not use `fast-boot2` with `balena-dev` because:
|
||||
// * fast-boot2's cacheKiller option is configured to include the timestamps of
|
||||
// the package.json and npm-shrinkwrap.json files, to avoid unexpected CLI
|
||||
// behavior when changes are made to dependencies during development. This is
|
||||
// generally a good thing, however, `balena-dev` (a few lines below) edits
|
||||
// `package.json` to modify oclif paths, and this results in cache
|
||||
// invalidation and a performance hit rather than speedup.
|
||||
// * Even if the timestamps are removed from cacheKiller, so that there is no
|
||||
// cache invalidation, fast-boot's speedup is barely noticeable when ts-node
|
||||
// is used, e.g. 1.43s vs 1.4s when running `balena version`.
|
||||
// * `fast-boot` causes unexpected behavior when used with `npm link` or
|
||||
// when the `node_modules` folder is manually modified (affecting transitive
|
||||
// dependencies) during development (e.g. bug investigations). A workaround
|
||||
// is to use `balena-dev` without `fast-boot`. See also notes in
|
||||
// `CONTRIBUTING.md`.
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
|
||||
@ -21,15 +35,14 @@ modifyOclifPaths();
|
||||
process.on('exit', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
// Undo changes in case of ctrl-v
|
||||
// Undo changes in case of ctrl-c
|
||||
process.on('SIGINT', function () {
|
||||
modifyOclifPaths(true);
|
||||
});
|
||||
|
||||
// Use fast-boot to cache require lookups, speeding up startup
|
||||
require('fast-boot2').start({
|
||||
cacheScope: __dirname + '/..',
|
||||
cacheFile: '.fast-boot.json',
|
||||
// Note process exit here will interfere with commands that do their own SIGINT handling,
|
||||
// but without it commands can not be exited.
|
||||
// So currently using balena-dev does not guarantee proper exit behaviour when using ctrl-c.
|
||||
// Ideally a better solution is needed.
|
||||
process.exit();
|
||||
});
|
||||
|
||||
// Set the desired es version for downstream modules that support it
|
||||
@ -44,7 +57,7 @@ require('ts-node').register({
|
||||
project: path.join(rootDir, 'tsconfig.json'),
|
||||
transpileOnly: true,
|
||||
});
|
||||
require('../lib/app').run();
|
||||
require('../lib/app').run(undefined, { dir: __dirname, development: true });
|
||||
|
||||
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||
function modifyOclifPaths(revert) {
|
||||
|
91
completion/_balena
Normal file
91
completion/_balena
Normal file
@ -0,0 +1,91 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands=( api-key api-keys app block build config deploy device device devices env envs fleet fleet fleets internal join key key keys leave local login logout logs notes orgs os preload push release release releases scan settings ssh support tag tags tunnel util version whoami )
|
||||
# Sub-completions
|
||||
api_key_cmds=( generate revoke )
|
||||
app_cmds=( create )
|
||||
block_cmds=( create )
|
||||
config_cmds=( generate inject read reconfigure write )
|
||||
device_cmds=( deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet )
|
||||
devices_cmds=( supported )
|
||||
env_cmds=( add rename rm )
|
||||
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||
internal_cmds=( osinit )
|
||||
key_cmds=( add rm )
|
||||
local_cmds=( configure flash )
|
||||
os_cmds=( build-config configure download initialize versions )
|
||||
release_cmds=( finalize invalidate validate )
|
||||
tag_cmds=( rm set )
|
||||
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
"api-key")
|
||||
_describe -t api_key_cmds 'api-key_cmd' api_key_cmds "$@" && ret=0
|
||||
;;
|
||||
"app")
|
||||
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
||||
;;
|
||||
"block")
|
||||
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
|
||||
;;
|
||||
"config")
|
||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||
;;
|
||||
"device")
|
||||
_describe -t device_cmds 'device_cmd' device_cmds "$@" && ret=0
|
||||
;;
|
||||
"devices")
|
||||
_describe -t devices_cmds 'devices_cmd' devices_cmds "$@" && ret=0
|
||||
;;
|
||||
"env")
|
||||
_describe -t env_cmds 'env_cmd' env_cmds "$@" && ret=0
|
||||
;;
|
||||
"fleet")
|
||||
_describe -t fleet_cmds 'fleet_cmd' fleet_cmds "$@" && ret=0
|
||||
;;
|
||||
"internal")
|
||||
_describe -t internal_cmds 'internal_cmd' internal_cmds "$@" && ret=0
|
||||
;;
|
||||
"key")
|
||||
_describe -t key_cmds 'key_cmd' key_cmds "$@" && ret=0
|
||||
;;
|
||||
"local")
|
||||
_describe -t local_cmds 'local_cmd' local_cmds "$@" && ret=0
|
||||
;;
|
||||
"os")
|
||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||
;;
|
||||
"release")
|
||||
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||
;;
|
||||
"tag")
|
||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||
;;
|
||||
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
88
completion/balena-completion.bash
Normal file
88
completion/balena-completion.bash
Normal file
@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
#GENERATED FILE DON'T MODIFY#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
main_commands="api-key api-keys app block build config deploy device device devices env envs fleet fleet fleets internal join key key keys leave local login logout logs notes orgs os preload push release release releases scan settings ssh support tag tags tunnel util version whoami"
|
||||
# Sub-completions
|
||||
api_key_cmds="generate revoke"
|
||||
app_cmds="create"
|
||||
block_cmds="create"
|
||||
config_cmds="generate inject read reconfigure write"
|
||||
device_cmds="deactivate identify init local-mode move os-update pin public-url purge reboot register rename restart rm shutdown start-service stop-service track-fleet"
|
||||
devices_cmds="supported"
|
||||
env_cmds="add rename rm"
|
||||
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||
internal_cmds="osinit"
|
||||
key_cmds="add rm"
|
||||
local_cmds="configure flash"
|
||||
os_cmds="build-config configure download initialize versions"
|
||||
release_cmds="finalize invalidate validate"
|
||||
tag_cmds="rm set"
|
||||
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
api-key)
|
||||
COMPREPLY=( $(compgen -W "$api_key_cmds" -- $cur) )
|
||||
;;
|
||||
app)
|
||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||
;;
|
||||
block)
|
||||
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
|
||||
;;
|
||||
config)
|
||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||
;;
|
||||
device)
|
||||
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
|
||||
;;
|
||||
devices)
|
||||
COMPREPLY=( $(compgen -W "$devices_cmds" -- $cur) )
|
||||
;;
|
||||
env)
|
||||
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
|
||||
;;
|
||||
fleet)
|
||||
COMPREPLY=( $(compgen -W "$fleet_cmds" -- $cur) )
|
||||
;;
|
||||
internal)
|
||||
COMPREPLY=( $(compgen -W "$internal_cmds" -- $cur) )
|
||||
;;
|
||||
key)
|
||||
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
|
||||
;;
|
||||
local)
|
||||
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
|
||||
;;
|
||||
os)
|
||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||
;;
|
||||
release)
|
||||
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||
;;
|
||||
tag)
|
||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||
;;
|
||||
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
175
completion/generate-completion.js
Normal file
175
completion/generate-completion.js
Normal file
@ -0,0 +1,175 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const fs = require('fs');
|
||||
const manifestFile = 'oclif.manifest.json';
|
||||
|
||||
commandsFilePath = path.join(rootDir, manifestFile);
|
||||
if (fs.existsSync(commandsFilePath)) {
|
||||
console.log('Generating shell auto completion files...');
|
||||
} else {
|
||||
console.error(`generate-completion.js: Could not find "${manifestFile}"`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||
|
||||
const mainCommands = [];
|
||||
const additionalCommands = [];
|
||||
for (const key of Object.keys(commandsJson.commands)) {
|
||||
const cmd = key.split(':');
|
||||
if (cmd.length > 1) {
|
||||
additionalCommands.push(cmd);
|
||||
if (!mainCommands.includes(cmd[0])) {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
} else {
|
||||
mainCommands.push(cmd[0]);
|
||||
}
|
||||
}
|
||||
const mainCommandsStr = mainCommands.join(' ');
|
||||
|
||||
// GENERATE BASH COMPLETION FILE
|
||||
bashFilePathIn = path.join(__dirname, '/templates/bash.template');
|
||||
bashFilePathOut = path.join(__dirname, 'balena-completion.bash');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(bashFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR BASH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands="' + mainCommandsStr + '"',
|
||||
);
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '="' + subCommands.join(' ') + '"\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' ' +
|
||||
prevElement +
|
||||
')\n COMPREPLY=( $(compgen -W "$' +
|
||||
prevElement2 +
|
||||
'" -- $cur) )\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(bashFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// GENERATE ZSH COMPLETION FILE
|
||||
zshFilePathIn = path.join(__dirname, '/templates/zsh.template');
|
||||
zshFilePathOut = path.join(__dirname, '_balena');
|
||||
|
||||
try {
|
||||
fs.unlinkSync(zshFilePathOut);
|
||||
} catch (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
||||
if (err) {
|
||||
process.exitCode = 1;
|
||||
return console.error(err);
|
||||
}
|
||||
|
||||
data = data.replace(
|
||||
'#TEMPLATE FILE FOR ZSH COMPLETION#',
|
||||
"#GENERATED FILE DON'T MODIFY#",
|
||||
);
|
||||
|
||||
data = data.replace(
|
||||
/\$main_commands\$/g,
|
||||
'main_commands=( ' + mainCommandsStr + ' )',
|
||||
);
|
||||
let subCommands = [];
|
||||
let prevElement = additionalCommands[0][0];
|
||||
additionalCommands.forEach(function (element) {
|
||||
if (element[0] === prevElement) {
|
||||
subCommands.push(element[1]);
|
||||
} else {
|
||||
const prevElement2 = prevElement.replace(/-/g, '_') + '_cmds';
|
||||
data = data.replace(
|
||||
/\$sub_cmds\$/g,
|
||||
' ' + prevElement2 + '=( ' + subCommands.join(' ') + ' )\n$sub_cmds$',
|
||||
);
|
||||
data = data.replace(
|
||||
/\$sub_cmds_prev\$/g,
|
||||
' "' +
|
||||
prevElement +
|
||||
'")\n _describe -t ' +
|
||||
prevElement2 +
|
||||
" '" +
|
||||
prevElement +
|
||||
"_cmd' " +
|
||||
prevElement2 +
|
||||
' "$@" && ret=0\n ;;\n$sub_cmds_prev$',
|
||||
);
|
||||
prevElement = element[0];
|
||||
subCommands = [];
|
||||
subCommands.push(element[1]);
|
||||
}
|
||||
});
|
||||
// cleanup placeholders
|
||||
data = data.replace(/\$sub_cmds\$/g, '');
|
||||
data = data.replace(/\$sub_cmds_prev\$/g, '');
|
||||
|
||||
fs.writeFile(zshFilePathOut, data, 'utf8', function (error) {
|
||||
if (error) {
|
||||
process.exitCode = 1;
|
||||
return console.error(error);
|
||||
}
|
||||
});
|
||||
});
|
32
completion/templates/bash.template
Normal file
32
completion/templates/bash.template
Normal file
@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
#TEMPLATE FILE FOR BASH COMPLETION#
|
||||
|
||||
_balena_complete()
|
||||
{
|
||||
local cur prev
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]}
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]
|
||||
then
|
||||
COMPREPLY=( $(compgen -W "${main_commands}" -- $cur) )
|
||||
elif [ $COMP_CWORD -eq 2 ]
|
||||
then
|
||||
case "$prev" in
|
||||
$sub_cmds_prev$
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
}
|
||||
complete -F _balena_complete balena
|
35
completion/templates/zsh.template
Normal file
35
completion/templates/zsh.template
Normal file
@ -0,0 +1,35 @@
|
||||
#compdef balena
|
||||
#autoload
|
||||
|
||||
#TEMPLATE FILE FOR ZSH COMPLETION#
|
||||
|
||||
_balena() {
|
||||
typeset -A opt_args
|
||||
local context state line curcontext="$curcontext"
|
||||
|
||||
# Valid top-level completions
|
||||
$main_commands$
|
||||
# Sub-completions
|
||||
$sub_cmds$
|
||||
|
||||
_arguments -C \
|
||||
'(- 1 *)--version[show version and exit]' \
|
||||
'(- 1 *)'{-h,--help}'[show help options and exit]' \
|
||||
'1:first command:_balena_main_cmds' \
|
||||
'2:second command:_balena_sec_cmds' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_main_cmds] )) ||
|
||||
_balena_main_cmds() {
|
||||
_describe -t main_commands 'command' main_commands "$@" && ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_balena_sec_cmds] )) ||
|
||||
_balena_sec_cmds() {
|
||||
case $line[1] in
|
||||
$sub_cmds_prev$
|
||||
esac
|
||||
}
|
||||
|
||||
_balena "$@"
|
@ -1,112 +0,0 @@
|
||||
# Provisioning balena devices in automated (non-interactive) mode
|
||||
|
||||
This document describes how to run the `device init` command in non-interactive mode.
|
||||
|
||||
It requires collecting some preliminary information _once_.
|
||||
|
||||
The final command to provision the device looks like this:
|
||||
|
||||
```bash
|
||||
balena device init --app APP_ID --os-version OS_VERSION --drive DRIVE --config CONFIG_FILE --yes
|
||||
|
||||
```
|
||||
|
||||
You can run this command as many times as you need, putting the new medium (SD card / USB stick) each time.
|
||||
|
||||
But before you can run it you need to collect the parameters and build the configuration file. Keep reading to figure out how to do it.
|
||||
|
||||
|
||||
## Collect all the required parameters.
|
||||
|
||||
1. `DEVICE_TYPE`. Run
|
||||
```bash
|
||||
balena devices supported
|
||||
```
|
||||
and find the _slug_ for your target device type, like _raspberrypi3_.
|
||||
|
||||
1. `APP_ID`. Create an application (`balena app create APP_NAME --type DEVICE_TYPE`) or find an existing one (`balena apps`) and notice its ID.
|
||||
|
||||
1. `OS_VERSION`. Run
|
||||
```bash
|
||||
balena os versions DEVICE_TYPE
|
||||
```
|
||||
and pick the version that you need, like _v2.0.6+rev1.prod_.
|
||||
_Note_ that even though we support _semver ranges_ it's recommended to use the exact version when doing the automated provisioning as it
|
||||
guarantees full compatibility between the steps.
|
||||
|
||||
1. `DRIVE`. Plug in your target medium (SD card or the USB stick, depending on your device type) and run
|
||||
```bash
|
||||
balena util available-drives
|
||||
```
|
||||
and get the drive name, like _/dev/sdb_ or _/dev/mmcblk0_.
|
||||
The balena CLI will not display the system drives to protect you,
|
||||
but still please check very carefully that you've picked the correct drive as it will be erased during the provisioning process.
|
||||
|
||||
Now we have all the parameters -- time to build the config file.
|
||||
|
||||
## Build the config file
|
||||
|
||||
Interactive device provisioning process often includes collecting some extra device configuration, like the networking mode and wifi credentials.
|
||||
|
||||
To skip this interactive step we need to buid this configuration once and save it to the JSON file for later reuse.
|
||||
|
||||
Let's say we will place it into the `CONFIG_FILE` path, like _./balena-os/raspberrypi3-config.json_.
|
||||
|
||||
We also need to put the OS image somewhere, let's call this path `OS_IMAGE_PATH`, it can be something like _./balena-os/raspberrypi3-v2.0.6+rev1.prod.img_.
|
||||
|
||||
1. First we need to download the OS image once. That's needed for building the config, and will speedup the subsequent operations as the downloaded OS image is placed into the local cache.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
balena os download DEVICE_TYPE --output OS_IMAGE_PATH --version OS_VERSION
|
||||
```
|
||||
|
||||
1. Now we're ready to build the config:
|
||||
|
||||
```bash
|
||||
balena os build-config OS_IMAGE_PATH DEVICE_TYPE --output CONFIG_FILE
|
||||
```
|
||||
|
||||
This will run you through the interactive configuration wizard and in the end save the generated config as `CONFIG_FILE`. You can then verify it's not empty:
|
||||
|
||||
```bash
|
||||
cat CONFIG_FILE
|
||||
```
|
||||
|
||||
## Done
|
||||
|
||||
Now you're ready to run the command in the beginning of this guide.
|
||||
|
||||
Please note again that all of these steps only need to be done once (unless you need to change something), and once all the parameters are collected the main init command can be run unchanged.
|
||||
|
||||
But there are still some nuances to cover, please read below.
|
||||
|
||||
## Nuances
|
||||
|
||||
### `sudo` password on *nix systems
|
||||
|
||||
In order to write the image to the raw device we need the root permissions, this is unavoidable.
|
||||
|
||||
To improve the security we only run the minimal subcommand with `sudo`.
|
||||
|
||||
This means that with the default setup you're interrupted closer to the end of the device init process to enter your sudo password for this subcommand to work.
|
||||
|
||||
There are several ways to eliminate it and make the process fully non-interactive.
|
||||
|
||||
#### Option 1: make passwordless sudo.
|
||||
|
||||
Obviously you shouldn't do that if the machine you're working on has access to any sensitive resources or information.
|
||||
|
||||
But if you're using a machine dedicated to balena provisioning this can be fine, and also the simplest thing to do.
|
||||
|
||||
#### Option 2: `NOPASSWD` directive
|
||||
|
||||
You can configure the `balena` CLI command to be sudo-runnable without the password. Check [this post](https://askubuntu.com/questions/159007/how-do-i-run-specific-sudo-commands-without-a-password) for an example.
|
||||
|
||||
### Extra initialization config
|
||||
|
||||
As of June 2017 all the supported devices should not require any other interactive configuration.
|
||||
|
||||
But by the design of our system it is _possible_ (though it doesn't look very likely it's going to happen any time soon) that some extra initialization options may be requested for the specific device types.
|
||||
|
||||
If that is the case please raise the issue in the balena CLI repository and the maintainers will add the necessary options to build the similar JSON config for this step.
|
File diff suppressed because it is too large
Load Diff
15
gulpfile.js
15
gulpfile.js
@ -1,15 +0,0 @@
|
||||
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')),
|
||||
);
|
111
lib/app.ts
111
lib/app.ts
@ -16,8 +16,15 @@
|
||||
*/
|
||||
|
||||
import * as packageJSON from '../package.json';
|
||||
import {
|
||||
AppOptions,
|
||||
checkDeletedCommand,
|
||||
preparseArgs,
|
||||
unsupportedFlag,
|
||||
} from './preparser';
|
||||
import { CliSettings } from './utils/bootstrap';
|
||||
import { onceAsync, stripIndent } from './utils/lazy';
|
||||
import { onceAsync } from './utils/lazy';
|
||||
import { run as mainRun, settings } from '@oclif/core';
|
||||
|
||||
/**
|
||||
* Sentry.io setup
|
||||
@ -27,6 +34,7 @@ export const setupSentry = onceAsync(async () => {
|
||||
const config = await import('./config');
|
||||
const Sentry = await import('@sentry/node');
|
||||
Sentry.init({
|
||||
autoSessionTracking: false,
|
||||
dsn: config.sentryDsn,
|
||||
release: packageJSON.version,
|
||||
});
|
||||
@ -43,13 +51,8 @@ export const setupSentry = onceAsync(async () => {
|
||||
async function checkNodeVersion() {
|
||||
const validNodeVersions = packageJSON.engines.node;
|
||||
if (!(await import('semver')).satisfies(process.version, validNodeVersions)) {
|
||||
console.warn(stripIndent`
|
||||
------------------------------------------------------------------------------
|
||||
Warning: Node version "${process.version}" does not match required versions "${validNodeVersions}".
|
||||
This may cause unexpected behavior. To upgrade Node, visit:
|
||||
https://nodejs.org/en/download/
|
||||
------------------------------------------------------------------------------
|
||||
`);
|
||||
const { getNodeEngineVersionWarn } = await import('./utils/messages');
|
||||
console.warn(getNodeEngineVersionWarn(process.version, validNodeVersions));
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,11 +78,13 @@ export function setMaxListeners(maxListeners: number) {
|
||||
/** Selected CLI initialization steps */
|
||||
async function init() {
|
||||
if (process.env.BALENARC_NO_SENTRY) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`WARN: disabling Sentry.io error reporting`);
|
||||
}
|
||||
} else {
|
||||
await setupSentry();
|
||||
}
|
||||
checkNodeVersion();
|
||||
await checkNodeVersion();
|
||||
|
||||
const settings = new CliSettings();
|
||||
|
||||
@ -89,43 +94,76 @@ async function init() {
|
||||
setupBalenaSdkSharedOptions(settings);
|
||||
|
||||
// check for CLI updates once a day
|
||||
(await import('./utils/update')).notify();
|
||||
if (!process.env.BALENARC_OFFLINE_MODE) {
|
||||
(await import('./utils/update')).notify();
|
||||
}
|
||||
}
|
||||
|
||||
/** Execute the oclif parser and the CLI command. */
|
||||
async function oclifRun(
|
||||
command: string[],
|
||||
options: import('./preparser').AppOptions,
|
||||
) {
|
||||
const { CustomMain } = await import('./utils/oclif-utils');
|
||||
const runPromise = CustomMain.run(command).then(
|
||||
() => {
|
||||
if (!options.noFlush) {
|
||||
return require('@oclif/command/flush');
|
||||
async function oclifRun(command: string[], options: AppOptions) {
|
||||
let deprecationPromise: Promise<void>;
|
||||
// check and enforce the CLI's deprecation policy
|
||||
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||
deprecationPromise = Promise.resolve();
|
||||
} else {
|
||||
const { DeprecationChecker } = await import('./deprecation');
|
||||
const deprecationChecker = new DeprecationChecker(packageJSON.version);
|
||||
// warnAndAbortIfDeprecated uses previously cached data only
|
||||
await deprecationChecker.warnAndAbortIfDeprecated();
|
||||
// checkForNewReleasesIfNeeded may query the npm registry
|
||||
deprecationPromise = deprecationChecker.checkForNewReleasesIfNeeded();
|
||||
}
|
||||
|
||||
const runPromise = (async function (shouldFlush: boolean) {
|
||||
let isEEXIT = false;
|
||||
try {
|
||||
if (options.development) {
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
settings.debug = true;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// oclif sometimes exits with ExitError code 0 (not an error)
|
||||
// For posteriority: We can't use default oclif 'execute' as
|
||||
// We customize error handling and flushing
|
||||
await mainRun(command, options.loadOptions ?? options.dir);
|
||||
} catch (error) {
|
||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||
// for example the `balena help` command.
|
||||
// (Avoid `error instanceof ExitError` here for the reasons explained
|
||||
// in the CONTRIBUTING.md file regarding the `instanceof` operator.)
|
||||
if (error.oclif?.exit === 0) {
|
||||
return;
|
||||
isEEXIT = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
if (shouldFlush) {
|
||||
await import('@oclif/core/flush');
|
||||
}
|
||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
||||
// addition to calling it in the main `run()` function in this file.
|
||||
// If it is not called here as well, there is a process exit delay of
|
||||
// 1 second when the fast-boot2 cache is modified (1 second is the
|
||||
// default cache saving timeout). Try for example `balena help`.
|
||||
// I have found that, when oclif's `Error: EEXIT: 0` is caught in
|
||||
// the try/catch block above, execution does not get past the
|
||||
// Promise.all() call below, but I don't understand why.
|
||||
if (isEEXIT) {
|
||||
(await import('./fast-boot')).stop();
|
||||
}
|
||||
})(!options.noFlush);
|
||||
|
||||
const { trackPromise } = await import('./hooks/prerun/track');
|
||||
await Promise.all([trackPromise, runPromise]);
|
||||
|
||||
await Promise.all([trackPromise, deprecationPromise, runPromise]);
|
||||
}
|
||||
|
||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||
export async function run(
|
||||
cliArgs = process.argv,
|
||||
options: import('./preparser').AppOptions = {},
|
||||
) {
|
||||
export async function run(cliArgs = process.argv, options: AppOptions) {
|
||||
try {
|
||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
||||
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||
'./utils/bootstrap'
|
||||
);
|
||||
setOfflineModeEnvVars();
|
||||
normalizeEnvVars();
|
||||
|
||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||
@ -136,8 +174,6 @@ export async function run(
|
||||
|
||||
await init();
|
||||
|
||||
const { preparseArgs, checkDeletedCommand } = await import('./preparser');
|
||||
|
||||
// Look for commands that have been removed and if so, exit with a notice
|
||||
checkDeletedCommand(cliArgs.slice(2));
|
||||
|
||||
@ -146,6 +182,13 @@ export async function run(
|
||||
} catch (err) {
|
||||
await (await import('./errors')).handleError(err);
|
||||
} finally {
|
||||
try {
|
||||
(await import('./fast-boot')).stop();
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Stopping fast-boot: ${e}`);
|
||||
}
|
||||
}
|
||||
// Windows fix: reading from stdin prevents the process from exiting
|
||||
process.stdin.pause();
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ export async function login({ host = '127.0.0.1', port = 0 }) {
|
||||
|
||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||
const open = await import('open');
|
||||
open(loginUrl, { wait: false });
|
||||
await open(loginUrl, { wait: false });
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const token = await loginServer.awaitForToken();
|
||||
|
@ -59,14 +59,15 @@ export class LoginServer extends EventEmitter {
|
||||
app.set('views', path.join(__dirname, 'pages'));
|
||||
|
||||
this.server = await new Promise<import('net').Server>((resolve, reject) => {
|
||||
const server = app.listen(port, host, (err: Error) => {
|
||||
const callback = (err: Error) => {
|
||||
if (err) {
|
||||
this.emit('error', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(server);
|
||||
}
|
||||
});
|
||||
};
|
||||
const server = app.listen(port, host, callback as any);
|
||||
server.on('connection', (socket) => this.serverSockets.push(socket));
|
||||
});
|
||||
|
||||
|
@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as url from 'url';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
|
||||
/**
|
||||
* @summary Get dashboard CLI login URL
|
||||
* @function
|
||||
* @protected
|
||||
* Get dashboard CLI login URL
|
||||
*
|
||||
* @param {String} callbackUrl - callback url
|
||||
* @fulfil {String} - dashboard login url
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
|
||||
* console.log(url)
|
||||
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
|
||||
* @returns Dashboard login URL, e.g.:
|
||||
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
|
||||
*/
|
||||
export const getDashboardLoginURL = (callbackUrl: string) => {
|
||||
export async function getDashboardLoginURL(
|
||||
callbackUrl: string,
|
||||
): Promise<string> {
|
||||
// Encode percentages signs from the escaped url
|
||||
// characters to avoid angular getting confused.
|
||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
|
||||
|
||||
return getBalenaSdk()
|
||||
.settings.get('dashboardUrl')
|
||||
.then((dashboardUrl) =>
|
||||
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
|
||||
);
|
||||
};
|
||||
const [{ URL }, dashboardUrl] = await Promise.all([
|
||||
import('url'),
|
||||
getBalenaSdk().settings.get('dashboardUrl'),
|
||||
]);
|
||||
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Log in using a token, but only if the token is valid
|
||||
* @function
|
||||
* @protected
|
||||
* Log in using a token, but only if the token is valid.
|
||||
*
|
||||
* @description
|
||||
* This function checks that the token is not only well-structured
|
||||
* but that it also authenticates with the server successfully.
|
||||
*
|
||||
* If authenticated, the token is persisted, if not then the previous
|
||||
* login state is restored.
|
||||
*
|
||||
* @param {String} token - session token or api key
|
||||
* @fulfil {Boolean} - whether the login was successful or not
|
||||
* @returns {Promise}
|
||||
*
|
||||
* utils.loginIfTokenValid('...').then (loggedIn) ->
|
||||
* if loggedIn
|
||||
* console.log('Token is valid!')
|
||||
* @param token - session token or api key
|
||||
* @returns whether the login was successful or not
|
||||
*/
|
||||
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
||||
if (_.isEmpty(token?.trim())) {
|
||||
export async function loginIfTokenValid(token?: string): Promise<boolean> {
|
||||
token = (token || '').trim();
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
const balena = getBalenaSdk();
|
||||
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
return isLoggedIn;
|
||||
};
|
||||
}
|
||||
|
@ -15,8 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Command from '@oclif/command';
|
||||
import { InsufficientPrivilegesError } from './errors';
|
||||
import { Command } from '@oclif/core';
|
||||
import {
|
||||
InsufficientPrivilegesError,
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
@ -40,6 +45,13 @@ export default abstract class BalenaCommand extends Command {
|
||||
*/
|
||||
public static authenticated = false;
|
||||
|
||||
/**
|
||||
* Require an internet connection to run.
|
||||
* When set to true, command will exit with an error
|
||||
* if user is running in offline mode (BALENARC_OFFLINE_MODE).
|
||||
*/
|
||||
public static offlineCompatible = false;
|
||||
|
||||
/**
|
||||
* Accept piped input.
|
||||
* When set to true, command will read from stdin during init
|
||||
@ -97,6 +109,29 @@ export default abstract class BalenaCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw NotAvailableInOfflineModeError if in offline mode.
|
||||
*
|
||||
* Called automatically if `onlineOnly=true`.
|
||||
* Can be called explicitly by command implementation, if e.g.:
|
||||
* - check should only be done conditionally
|
||||
* - other code needs to execute before check
|
||||
*
|
||||
* Note, currently public to allow use outside of derived commands
|
||||
* (as some command implementations require this. Can be made protected
|
||||
* if this changes).
|
||||
*
|
||||
* @throws {NotAvailableInOfflineModeError}
|
||||
*/
|
||||
public static checkNotUsingOfflineMode() {
|
||||
if (process.env.BALENARC_OFFLINE_MODE) {
|
||||
throw new NotAvailableInOfflineModeError(stripIndent`
|
||||
This command requires an internet connection, and cannot be used in offline mode.
|
||||
To leave offline mode, unset the BALENARC_OFFLINE_MODE environment variable.
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read stdin contents and make available to command.
|
||||
*
|
||||
@ -125,8 +160,15 @@ export default abstract class BalenaCommand extends Command {
|
||||
await BalenaCommand.checkLoggedIn();
|
||||
}
|
||||
|
||||
if (!ctr.offlineCompatible) {
|
||||
BalenaCommand.checkNotUsingOfflineMode();
|
||||
}
|
||||
|
||||
if (ctr.readStdin) {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
}
|
||||
|
@ -15,20 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class GenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a new balenaCloud API key.
|
||||
@ -41,24 +33,23 @@ export default class GenerateCmd extends Command {
|
||||
`;
|
||||
public static examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'the API key name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'api-key generate <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
||||
const { args: params } = await this.parse(GenerateCmd);
|
||||
|
||||
let key;
|
||||
try {
|
||||
|
67
lib/commands/api-key/revoke.ts
Normal file
67
lib/commands/api-key/revoke.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class RevokeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Revoke balenaCloud API keys.
|
||||
|
||||
Revoke balenaCloud API keys with the given
|
||||
comma-separated list of ids.
|
||||
|
||||
The given balenaCloud API keys will no longer be usable.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena api-key revoke 123',
|
||||
'$ balena api-key revoke 123,124,456',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
ids: Args.string({
|
||||
description: 'the API key ids',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'api-key revoke <ids>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(RevokeCmd);
|
||||
|
||||
const apiKeyIds = params.ids.split(',');
|
||||
if (apiKeyIds.filter((apiKeyId) => !apiKeyId.match(/^\d+$/)).length > 0) {
|
||||
console.log('API key ids must be positive integers');
|
||||
return;
|
||||
}
|
||||
await Promise.all(
|
||||
apiKeyIds.map(
|
||||
async (id) => await getBalenaSdk().models.apiKey.revoke(Number(id)),
|
||||
),
|
||||
);
|
||||
console.log('Successfully revoked the given API keys');
|
||||
}
|
||||
}
|
83
lib/commands/api-keys/index.ts
Normal file
83
lib/commands/api-keys/index.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/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class ApiKeysCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Print a list of balenaCloud API keys.
|
||||
|
||||
Print a list of balenaCloud API keys.
|
||||
|
||||
Print a list of balenaCloud API keys for the current user or for a specific fleet with the \`--fleet\` option.
|
||||
`;
|
||||
public static examples = ['$ balena api-keys'];
|
||||
|
||||
public static usage = 'api-keys';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
user: Flags.boolean({
|
||||
char: 'u',
|
||||
description: 'show API keys for your user',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = await this.parse(ApiKeysCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const actorId = options.fleet
|
||||
? (
|
||||
await getApplication(getBalenaSdk(), options.fleet, {
|
||||
$select: 'actor',
|
||||
})
|
||||
).actor
|
||||
: await getBalenaSdk().auth.getActorId();
|
||||
const keys = await getBalenaSdk().pine.get({
|
||||
resource: 'api_key',
|
||||
options: {
|
||||
$select: ['id', 'created_at', 'name', 'description', 'expiry_date'],
|
||||
$filter: {
|
||||
is_of__actor: actorId,
|
||||
...(options.user
|
||||
? {
|
||||
name: {
|
||||
$ne: null,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
$orderby: 'name asc',
|
||||
},
|
||||
});
|
||||
const fields = ['id', 'name', 'created_at', 'description', 'expiry_date'];
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
keys.map((key) => _.mapValues(key, (val) => val ?? 'N/a')),
|
||||
fields,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,55 +15,58 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import type * as BalenaSDK from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
type?: string; // application device type
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class AppCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an application.
|
||||
Create an app.
|
||||
|
||||
Create a new balena application.
|
||||
Create a new balena app.
|
||||
|
||||
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 specify the organization the app should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
You can see a list of supported device types with:
|
||||
The app's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
$ balena devices supported
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena app create MyApp',
|
||||
'$ balena app create MyApp --type raspberry-pi',
|
||||
'$ balena app create MyApp --organization mmyorg',
|
||||
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name',
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'app name',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'app create <name>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
public static flags = {
|
||||
organization: Flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the app should belong to',
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'application device type (Check available types with `balena devices supported`)',
|
||||
'app device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
@ -71,34 +74,10 @@ export default class AppCreateCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppCreateCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(AppCreateCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Create application
|
||||
const deviceType =
|
||||
options.type ||
|
||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
||||
let application: BalenaSDK.Application;
|
||||
try {
|
||||
application = await balena.models.application.create({
|
||||
name: params.name,
|
||||
deviceType,
|
||||
organization: (await balena.auth.whoami())!,
|
||||
});
|
||||
} 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} (${deviceType}, id ${application.id})`,
|
||||
);
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('app', options, params);
|
||||
}
|
||||
}
|
||||
|
@ -1,92 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import type { Release } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
nameOrSlug: 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', '$ balena app myorg/myapp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'nameOrSlug',
|
||||
description: 'application name or org/name slug',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app <nameOrSlug>';
|
||||
|
||||
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 { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const application = (await getApplication(
|
||||
getBalenaSdk(),
|
||||
params.nameOrSlug,
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
},
|
||||
)) as ApplicationWithDeviceType & {
|
||||
should_be_running__release: [Release?];
|
||||
// For display purposes:
|
||||
device_type: string;
|
||||
commit?: string;
|
||||
};
|
||||
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
/**
|
||||
* @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 Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { lowercaseIfSlug } from '../../utils/normalization';
|
||||
import type { ApplicationType } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
nameOrSlug: string;
|
||||
newName?: string;
|
||||
}
|
||||
|
||||
export default class AppRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename an application.
|
||||
|
||||
Rename an application.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app rename OldName',
|
||||
'$ balena app rename OldName NewName',
|
||||
'$ balena app rename myorg/oldname NewName',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'nameOrSlug',
|
||||
description: 'application name or org/name slug',
|
||||
parse: lowercaseIfSlug,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
description: 'the new name for the application',
|
||||
},
|
||||
];
|
||||
|
||||
public static usage = 'app rename <nameOrSlug> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate target application (if nameOrSlug is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, params.nameOrSlug, {
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: ['is_legacy'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check app exists
|
||||
if (!application) {
|
||||
throw new ExpectedError(
|
||||
'Error: application ${params.nameOrSlug} not found.',
|
||||
);
|
||||
}
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = (application.application_type as ApplicationType[])?.[0];
|
||||
if (appType.is_legacy) {
|
||||
throw new ExpectedError(
|
||||
`Application ${params.nameOrSlug} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ascertain new name
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this application:',
|
||||
type: 'input',
|
||||
validate: validateApplicationName,
|
||||
})) ||
|
||||
'';
|
||||
|
||||
// Rename
|
||||
try {
|
||||
await balena.models.application.rename(application.id, newName);
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(
|
||||
`Error: application ${params.nameOrSlug} already exists.`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Output result
|
||||
console.log(`Application ${params.nameOrSlug} renamed to ${newName}`);
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
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({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'No-op since release v12.0.0',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(AppsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Get applications
|
||||
const applications = (await balena.models.application.getAll({
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
const _ = await import('lodash');
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
// @ts-expect-error
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
'app_name',
|
||||
options.verbose || 'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
83
lib/commands/block/create.ts
Normal file
83
lib/commands/block/create.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class BlockCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create an block.
|
||||
|
||||
Create a new balena block.
|
||||
|
||||
You can specify the organization the block should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
The block's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena block create MyBlock',
|
||||
'$ balena block create MyBlock --organization mmyorg',
|
||||
'$ balena block create MyBlock -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'block name',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'block create <name>';
|
||||
|
||||
public static flags = {
|
||||
organization: Flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the block should belong to',
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'block device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(BlockCreateCmd);
|
||||
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('block', options, params);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,29 +15,33 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../command';
|
||||
import { getBalenaSdk } from '../utils/lazy';
|
||||
import * as compose from '../utils/compose';
|
||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import { getBalenaSdk } from '../../utils/lazy';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as compose from '../../utils/compose';
|
||||
import type { ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||
import {
|
||||
buildArgDeprecation,
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
} from '../../utils/messages';
|
||||
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types';
|
||||
import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||
import { dockerCliFlags } from '../../utils/docker';
|
||||
|
||||
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
|
||||
// because of the 'registry-secrets' type which is defined in the actual code
|
||||
// as a path (string | undefined) but then the cli turns it into an object
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
arch?: string;
|
||||
deviceType?: string;
|
||||
application?: string;
|
||||
fleet?: string;
|
||||
source?: string; // Not part of command profile - source param copied here.
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export default class BuildCmd extends Command {
|
||||
public static description = `\
|
||||
Build a project locally.
|
||||
@ -47,7 +51,7 @@ 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.
|
||||
You must specify either a fleet, or the device type and architecture.
|
||||
|
||||
This command will look into the given source directory (or the current working
|
||||
directory if one isn't specified) for a docker-compose.yml file, and if found,
|
||||
@ -61,54 +65,45 @@ ${registrySecretsHelp}
|
||||
${dockerignoreHelp}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena build --application myApp',
|
||||
'$ balena build ./source/ --application myApp',
|
||||
'$ balena build --fleet myFleet',
|
||||
'$ balena build ./source/ --fleet myorg/myfleet',
|
||||
'$ balena build ./source/ --fleet myorg/myfleet --docker-compose my-custom-compose.yml',
|
||||
'$ balena build --deviceType raspberrypi3 --arch armv7hf --emulated',
|
||||
'$ balena build --docker /var/run/docker.sock --application myApp # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --application myApp # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -a myApp',
|
||||
'$ balena build --docker /var/run/docker.sock --fleet myFleet # Linux, Mac',
|
||||
'$ balena build --docker //./pipe/docker_engine --fleet myFleet # Windows',
|
||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'source',
|
||||
description: 'path of project source directory',
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
source: Args.string({ description: 'path of project source directory' }),
|
||||
};
|
||||
|
||||
public static usage = 'build [source]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
arch: flags.string({
|
||||
public static flags = {
|
||||
arch: Flags.string({
|
||||
description: 'the architecture to build for',
|
||||
char: 'A',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
deviceType: Flags.string({
|
||||
description: 'the type of device this build is for',
|
||||
char: 'd',
|
||||
}),
|
||||
application: flags.string({
|
||||
description: 'name of the target balena application this build is for',
|
||||
char: 'a',
|
||||
}),
|
||||
fleet: cf.fleet,
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
help: Flags.help({}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
BuildCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(BuildCmd);
|
||||
|
||||
await Command.checkLoggedInIf(!!options.application);
|
||||
await Command.checkLoggedInIf(!!options.fleet);
|
||||
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
@ -122,6 +117,11 @@ ${dockerignoreHelp}
|
||||
|
||||
await this.validateOptions(options, sdk);
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
const app = await this.getAppAndResolveArch(options);
|
||||
|
||||
const { docker, buildOpts, composeOpts } = await this.prepareBuild(options);
|
||||
@ -146,19 +146,17 @@ ${dockerignoreHelp}
|
||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||
// Validate option combinations
|
||||
if (
|
||||
(opts.application == null &&
|
||||
(opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.application != null &&
|
||||
(opts.arch != null || opts.deviceType != null))
|
||||
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||
) {
|
||||
const { ExpectedError } = await import('../errors');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
'You must specify either an application or an arch/deviceType pair to build for',
|
||||
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||
);
|
||||
}
|
||||
|
||||
// Validate project directory
|
||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
||||
const { validateProjectDirectory } = await import('../../utils/compose_ts');
|
||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||
sdk,
|
||||
{
|
||||
@ -174,9 +172,9 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||
if (opts.application) {
|
||||
const { getAppWithArch } = await import('../utils/helpers');
|
||||
const app = await getAppWithArch(opts.application);
|
||||
if (opts.fleet) {
|
||||
const { getAppWithArch } = await import('../../utils/helpers');
|
||||
const app = await getAppWithArch(opts.fleet);
|
||||
opts.arch = app.arch;
|
||||
opts.deviceType = app.is_for__device_type[0].slug;
|
||||
return app;
|
||||
@ -184,7 +182,7 @@ ${dockerignoreHelp}
|
||||
}
|
||||
|
||||
protected async prepareBuild(options: FlagsDef) {
|
||||
const { getDocker, generateBuildOpts } = await import('../utils/docker');
|
||||
const { getDocker, generateBuildOpts } = await import('../../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
getDocker(options),
|
||||
generateBuildOpts(options),
|
||||
@ -205,35 +203,42 @@ ${dockerignoreHelp}
|
||||
* buildEmulated
|
||||
* buildOpts: arguments to forward to docker build command
|
||||
*
|
||||
* @param {DockerToolbelt} docker
|
||||
* @param {Dockerode} docker
|
||||
* @param {Logger} logger
|
||||
* @param {ComposeOpts} composeOpts
|
||||
* @param opts
|
||||
*/
|
||||
protected async buildProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: import('../utils/logger'),
|
||||
docker: import('dockerode'),
|
||||
logger: import('../../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app?: Application;
|
||||
app?: {
|
||||
application_type: [Pick<ApplicationType, 'supports_multicontainer'>];
|
||||
};
|
||||
arch: string;
|
||||
deviceType: string;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: BuildOpts;
|
||||
},
|
||||
) {
|
||||
const { loadProject } = await import('../utils/compose_ts');
|
||||
const { loadProject } = await import('../../utils/compose_ts');
|
||||
|
||||
const project = await loadProject(logger, composeOpts);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
undefined,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
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' +
|
||||
'Target fleet does not support multiple containers.\n' +
|
||||
'Continuing with build, but you will not be able to deploy.',
|
||||
);
|
||||
}
|
||||
@ -251,7 +256,6 @@ ${dockerignoreHelp}
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
}
|
@ -15,182 +15,199 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags } from '@oclif/core';
|
||||
import type { Interfaces } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||
import type { PineDeferred } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
version: string; // OS version
|
||||
application?: string;
|
||||
app?: string; // application alias
|
||||
device?: string;
|
||||
deviceApiKey?: string;
|
||||
deviceType?: string;
|
||||
'generate-device-api-key': boolean;
|
||||
output?: string;
|
||||
// Options for non-interactive configuration
|
||||
network?: string;
|
||||
wifiSsid?: string;
|
||||
wifiKey?: string;
|
||||
appUpdatePollInterval?: string;
|
||||
help: void;
|
||||
}
|
||||
import {
|
||||
applicationIdInfo,
|
||||
devModeInfo,
|
||||
secureBootInfo,
|
||||
} from '../../utils/messages';
|
||||
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
||||
|
||||
export default class ConfigGenerateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Generate a config.json file.
|
||||
|
||||
Generate a config.json file for a device or application.
|
||||
Generate a config.json file for a device or fleet.
|
||||
|
||||
Calling this command with the exact version number of the targeted image is required.
|
||||
The target balenaOS version must be specified with the --version option.
|
||||
|
||||
This command 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.
|
||||
${devModeInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
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.
|
||||
${secureBootInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
||||
alongside the --deviceType option to specify the target device type.
|
||||
|
||||
To avoid interactive questions, specify a command line option for each question that
|
||||
would otherwise be asked.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --deviceApiKey <existingDeviceKey>',
|
||||
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --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',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
||||
];
|
||||
|
||||
public static usage = 'config generate';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
version: flags.string({
|
||||
public static flags = {
|
||||
version: Flags.string({
|
||||
description: 'a balenaOS version',
|
||||
required: true,
|
||||
}),
|
||||
application: flags.string({
|
||||
description: 'application name',
|
||||
char: 'a',
|
||||
exclusive: ['app', 'device'],
|
||||
}),
|
||||
app: flags.string({
|
||||
description: "same as '--application'",
|
||||
exclusive: ['application', 'device'],
|
||||
}),
|
||||
device: flags.string({
|
||||
description: 'device uuid',
|
||||
char: 'd',
|
||||
exclusive: ['application', 'app'],
|
||||
}),
|
||||
deviceApiKey: flags.string({
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
dev: cf.dev,
|
||||
secureBoot: cf.secureBoot,
|
||||
device: {
|
||||
...cf.device,
|
||||
exclusive: [
|
||||
'fleet',
|
||||
'provisioning-key-name',
|
||||
'provisioning-key-expiry-date',
|
||||
],
|
||||
},
|
||||
deviceApiKey: Flags.string({
|
||||
description:
|
||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||
char: 'k',
|
||||
}),
|
||||
deviceType: flags.string({
|
||||
description: 'device type slug',
|
||||
deviceType: Flags.string({
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
}),
|
||||
'generate-device-api-key': flags.boolean({
|
||||
'generate-device-api-key': Flags.boolean({
|
||||
description: 'generate a fresh device key for the device',
|
||||
}),
|
||||
output: flags.string({
|
||||
output: Flags.string({
|
||||
description: 'path of output file',
|
||||
char: 'o',
|
||||
}),
|
||||
// Options for non-interactive configuration
|
||||
network: flags.string({
|
||||
network: Flags.string({
|
||||
description: 'the network type to use: ethernet or wifi',
|
||||
options: ['ethernet', 'wifi'],
|
||||
}),
|
||||
wifiSsid: flags.string({
|
||||
wifiSsid: Flags.string({
|
||||
description:
|
||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
wifiKey: flags.string({
|
||||
wifiKey: Flags.string({
|
||||
description:
|
||||
'the wifi key to use (used only if --network is set to wifi)',
|
||||
}),
|
||||
appUpdatePollInterval: flags.string({
|
||||
appUpdatePollInterval: Flags.string({
|
||||
description:
|
||||
'how frequently (in minutes) to poll for application updates',
|
||||
'supervisor cloud polling interval in minutes (e.g. for device variables)',
|
||||
}),
|
||||
'provisioning-key-name': Flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
'provisioning-key-expiry-date': Flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
exclusive: ['device'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
||||
|
||||
public async getApplication(balena: BalenaSDK, fleet: string) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
return await getApplication(balena, fleet, {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = await this.parse(ConfigGenerateCmd);
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await this.validateOptions(options);
|
||||
|
||||
let resourceDeviceType: string;
|
||||
let application: ApplicationWithDeviceType | null = null;
|
||||
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
||||
null;
|
||||
let device:
|
||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||
| null = null;
|
||||
if (options.device != null) {
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const rawDevice = await balena.models.device.get(
|
||||
tryAsInteger(options.device),
|
||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
||||
);
|
||||
const rawDevice = await balena.models.device.get(options.device, {
|
||||
$expand: { is_of__device_type: { $select: 'slug' } },
|
||||
});
|
||||
if (!rawDevice.belongs_to__application) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(stripIndent`
|
||||
Device ${options.device} does not appear to belong to an accessible application.
|
||||
Try with a different device, or use '--application' instead of '--device'.`);
|
||||
Device ${options.device} does not appear to belong to an accessible fleet.
|
||||
Try with a different device, or use '--fleet' instead of '--device'.`);
|
||||
}
|
||||
device = rawDevice as DeviceWithDeviceType & {
|
||||
belongs_to__application: PineDeferred;
|
||||
};
|
||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||
} else {
|
||||
application = (await getApplication(balena, options.application!, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
},
|
||||
})) as ApplicationWithDeviceType;
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
application = await this.getApplication(balena, options.fleet!);
|
||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||
}
|
||||
|
||||
const deviceType = options.deviceType || resourceDeviceType;
|
||||
|
||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
||||
deviceType,
|
||||
);
|
||||
|
||||
// Check compatibility if application and deviceType provided
|
||||
if (options.application && options.deviceType) {
|
||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
||||
resourceDeviceType,
|
||||
);
|
||||
|
||||
if (options.fleet && options.deviceType) {
|
||||
const helpers = await import('../../utils/helpers');
|
||||
if (
|
||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
||||
!(await helpers.areDeviceTypesCompatible(
|
||||
resourceDeviceType,
|
||||
deviceType,
|
||||
))
|
||||
) {
|
||||
throw new balena.errors.BalenaInvalidDeviceType(
|
||||
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Device type ${options.deviceType} is incompatible with fleet ${options.fleet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deviceManifest =
|
||||
await balena.models.config.getDeviceTypeManifestBySlug(deviceType);
|
||||
|
||||
const { validateSecureBootOptionAndWarn } = await import(
|
||||
'../../utils/config'
|
||||
);
|
||||
await validateSecureBootOptionAndWarn(
|
||||
options.secureBoot,
|
||||
deviceType,
|
||||
options.version,
|
||||
);
|
||||
|
||||
// Prompt for values
|
||||
// Pass params as an override: if there is any param with exactly the same name as a
|
||||
// required option, that value is used (and the corresponding question is not asked)
|
||||
const answers = await getCliForm().run(deviceManifest.options, {
|
||||
override: options,
|
||||
override: { ...options, app: options.fleet, application: options.fleet },
|
||||
});
|
||||
answers.version = options.version;
|
||||
answers.developmentMode = options.dev;
|
||||
answers.secureBoot = options.secureBoot;
|
||||
answers.provisioningKeyName = options['provisioning-key-name'];
|
||||
answers.provisioningKeyExpiryDate = options['provisioning-key-expiry-date'];
|
||||
|
||||
// Generate config
|
||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||
@ -220,38 +237,31 @@ export default class ConfigGenerateCmd extends Command {
|
||||
}
|
||||
|
||||
protected readonly missingDeviceOrAppMessage = stripIndent`
|
||||
Either a device or an application must be specified.
|
||||
Either a device or a fleet must be specified.
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
`;
|
||||
|
||||
protected readonly deviceTypeNotAllowedMessage = stripIndent`
|
||||
Specifying a different device type is only supported when
|
||||
generating a config for an application:
|
||||
protected readonly deviceTypeNotAllowedMessage =
|
||||
'The --deviceType option can only be used alongside the --fleet option';
|
||||
|
||||
* An application, with --app <appname>
|
||||
* A specific device type, with --device-type <deviceTypeSlug>
|
||||
|
||||
See the help page for examples:
|
||||
|
||||
$ balena help config generate
|
||||
`;
|
||||
|
||||
protected async validateOptions(options: FlagsDef) {
|
||||
protected async validateOptions(
|
||||
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
|
||||
) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
// Prefer options.application over options.app
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
if (options.device == null && options.application == null) {
|
||||
if (options.device == null && options.fleet == null) {
|
||||
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
||||
}
|
||||
|
||||
if (!options.application && options.deviceType) {
|
||||
if (!options.fleet && options.deviceType) {
|
||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
||||
}
|
||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||
options.version = normalizeOsVersion(options.version);
|
||||
const { validateDevOptionAndWarn } = await import('../../utils/config');
|
||||
await validateDevOptionAndWarn(options.dev, options.version);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,73 +15,52 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
file: string;
|
||||
}
|
||||
|
||||
export default class ConfigInjectCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Inject a configuration file into a device or OS image.
|
||||
Inject a config.json file to a balenaOS image or attached media.
|
||||
|
||||
Inject a config.json file to the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config inject my/config.json --type raspberrypi3',
|
||||
'$ balena config inject my/config.json --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config inject my/config.json',
|
||||
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'file',
|
||||
public static args = {
|
||||
file: Args.string({
|
||||
description: 'the path to the config.json file to inject',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'config inject <file>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigInjectCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device/OS drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const fs = await import('fs');
|
||||
const configJSON = JSON.parse(
|
||||
@ -89,7 +68,7 @@ export default class ConfigInjectCmd extends Command {
|
||||
);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
await config.write(drive, options.type, configJSON);
|
||||
await config.write(drive, '', configJSON);
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,64 +15,55 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ConfigReadCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Read the configuration of a device or OS image.
|
||||
Read the config.json file of a balenaOS image or attached media.
|
||||
|
||||
Read the config.json file from the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Read the 'config.json' file of a balenaOS image file or attached SD card or
|
||||
USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config read --type raspberrypi3',
|
||||
'$ balena config read --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config read',
|
||||
'$ balena config read --drive /dev/disk2',
|
||||
'$ balena config read --drive balena.img',
|
||||
];
|
||||
|
||||
public static usage = 'config read';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
||||
const { flags: options } = await this.parse(ConfigReadCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
const configJSON = await config.read(drive, '');
|
||||
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.info(prettyjson.render(configJSON));
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(configJSON, null, 4));
|
||||
} else {
|
||||
const prettyjson = await import('prettyjson');
|
||||
console.log(prettyjson.render(configJSON));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,69 +15,71 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
advanced: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
export default class ConfigReconfigureCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Interactively reconfigure a device or OS image.
|
||||
Interactively reconfigure a balenaOS image file or attached media.
|
||||
|
||||
Interactively reconfigure a provisioned device or OS image.
|
||||
Interactively reconfigure a balenaOS image file or attached media.
|
||||
|
||||
This command extracts the device UUID from the 'config.json' file of the
|
||||
chosen balenaOS image file or attached media, and then passes the UUID as
|
||||
the '--device' argument to the 'balena os configure' command.
|
||||
|
||||
For finer-grained or scripted control of the operation, use the
|
||||
'balena config read' and 'balena os configure' commands separately.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena config reconfigure --type raspberrypi3',
|
||||
'$ balena config reconfigure --type raspberrypi3 --advanced',
|
||||
'$ balena config reconfigure --type raspberrypi3 --drive /dev/disk2',
|
||||
'$ balena config reconfigure',
|
||||
'$ balena config reconfigure --drive /dev/disk3',
|
||||
'$ balena config reconfigure --drive balena.img --advanced',
|
||||
];
|
||||
|
||||
public static usage = 'config reconfigure';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
advanced: flags.boolean({
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
advanced: Flags.boolean({
|
||||
description: 'show advanced commands',
|
||||
char: 'v',
|
||||
}),
|
||||
help: cf.help,
|
||||
version: Flags.string({
|
||||
description: 'balenaOS version, for example "2.32.0" or "2.44.0+rev1"',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
||||
const { flags: options } = await this.parse(ConfigReconfigureCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const { uuid } = await config.read(drive, options.type);
|
||||
await umountAsync(drive);
|
||||
const { uuid } = await config.read(drive, '');
|
||||
await safeUmount(drive);
|
||||
|
||||
if (!uuid) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
throw new ExpectedError(
|
||||
`Error: UUID not found in 'config.json' file for '${drive}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const configureCommand = ['os', 'configure', drive, '--device', uuid];
|
||||
if (options.version) {
|
||||
configureCommand.push('--version', options.version);
|
||||
}
|
||||
if (options.advanced) {
|
||||
configureCommand.push('--advanced');
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,92 +15,79 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
type: string;
|
||||
drive?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default class ConfigWriteCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Write a key-value pair to configuration of a device or OS image.
|
||||
Write a key-value pair to the config.json file of an OS image or attached media.
|
||||
|
||||
Write a key-value pair to the config.json file on the mounted filesystem,
|
||||
e.g. the SD card of a provisioned device or balenaOS image.
|
||||
Write a key-value pair to the 'config.json' file of a balenaOS image file or
|
||||
attached SD card or USB stick.
|
||||
|
||||
Documentation for the balenaOS 'config.json' file can be found at:
|
||||
https://www.balena.io/docs/reference/OS/configuration/
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena config write --type raspberrypi3 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 --drive /dev/disk2 username johndoe',
|
||||
'$ balena config write --type raspberrypi3 files.network/settings "..."',
|
||||
'$ balena config write ntpServers "0.resinio.pool.ntp.org 1.resinio.pool.ntp.org"',
|
||||
'$ balena config write --drive /dev/disk2 hostname custom-hostname',
|
||||
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'key',
|
||||
public static args = {
|
||||
key: Args.string({
|
||||
description: 'the key of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
}),
|
||||
value: Args.string({
|
||||
description: 'the value of the config parameter to write',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'config write <key> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
type: flags.string({
|
||||
description:
|
||||
'device type (Check available types with `balena devices supported`)',
|
||||
char: 't',
|
||||
required: true,
|
||||
}),
|
||||
drive: flags.string({
|
||||
description: 'device filesystem or OS image location',
|
||||
char: 'd',
|
||||
}),
|
||||
public static flags = {
|
||||
drive: cf.driveOrImg,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public static root = true;
|
||||
public static offlineCompatible = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
ConfigWriteCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
|
||||
|
||||
const { promisify } = await import('util');
|
||||
const umountAsync = promisify((await import('umount')).umount);
|
||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||
|
||||
const drive =
|
||||
options.drive || (await getVisuals().drive('Select the device drive'));
|
||||
await umountAsync(drive);
|
||||
await safeUmount(drive);
|
||||
|
||||
const config = await import('balena-config-json');
|
||||
const configJSON = await config.read(drive, options.type);
|
||||
const configJSON = await config.read(drive, '');
|
||||
|
||||
console.info(`Setting ${params.key} to ${params.value}`);
|
||||
const _ = await import('lodash');
|
||||
_.set(configJSON, params.key, params.value);
|
||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||
|
||||
await umountAsync(drive);
|
||||
|
||||
await config.write(drive, options.type, configJSON);
|
||||
await denyMount(drive, async () => {
|
||||
await safeUmount(drive);
|
||||
await config.write(drive, '', configJSON);
|
||||
});
|
||||
|
||||
console.info('Done');
|
||||
}
|
||||
|
||||
/** Call Lodash's _.setWith(). Moved here for ease of testing. */
|
||||
static updateConfigJson(configJSON: object, key: string, value: string) {
|
||||
const _ = require('lodash') as typeof import('lodash');
|
||||
// note: _.setWith() is needed instead of _.set() because, given a key
|
||||
// like `os.udevRules.101`, _.set() creates a udevRules array (rather
|
||||
// than a dictionary) and sets the 101st array element to value, while
|
||||
// we actually want udevRules to be dictionary like { '101': value }
|
||||
_.setWith(configJSON, key, value, (v) => v || {});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2020 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,52 +15,64 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { ImageDescriptor } from 'resin-compose-parse';
|
||||
import { Args, Flags } from '@oclif/core';
|
||||
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||
|
||||
import Command from '../command';
|
||||
import { ExpectedError } from '../errors';
|
||||
import { getBalenaSdk, getChalk } from '../utils/lazy';
|
||||
import { dockerignoreHelp, registrySecretsHelp } from '../utils/messages';
|
||||
import * as compose from '../utils/compose';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
|
||||
import {
|
||||
dockerignoreHelp,
|
||||
registrySecretsHelp,
|
||||
buildArgDeprecation,
|
||||
} from '../../utils/messages';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import * as compose from '../../utils/compose';
|
||||
import type {
|
||||
BuiltImage,
|
||||
ComposeCliFlags,
|
||||
ComposeOpts,
|
||||
} from '../utils/compose-types';
|
||||
import type { DockerCliFlags } from '../utils/docker';
|
||||
Release as ComposeReleaseInfo,
|
||||
} from '../../utils/compose-types';
|
||||
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||
import {
|
||||
applyReleaseTagKeysAndValues,
|
||||
buildProject,
|
||||
composeCliFlags,
|
||||
isBuildConfig,
|
||||
} from '../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../utils/docker';
|
||||
import type { Application, ApplicationType, DeviceType } from 'balena-sdk';
|
||||
parseReleaseTagKeysAndValues,
|
||||
} from '../../utils/compose_ts';
|
||||
import { dockerCliFlags } from '../../utils/docker';
|
||||
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
|
||||
|
||||
interface ApplicationWithArch extends Application {
|
||||
interface ApplicationWithArch {
|
||||
id: number;
|
||||
arch: string;
|
||||
is_for__device_type: [Pick<DeviceType, 'slug'>];
|
||||
application_type: [Pick<ApplicationType, 'slug' | 'supports_multicontainer'>];
|
||||
}
|
||||
|
||||
// TODO: For this special one we can't use Interfaces.InferredFlags/InferredArgs
|
||||
// because of the 'registry-secrets' type which is defined in the actual code
|
||||
// as a path (string | undefined) but then the cli turns it into an object
|
||||
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||
source?: string;
|
||||
build: boolean;
|
||||
nologupload: boolean;
|
||||
'release-tag'?: string[];
|
||||
draft: boolean;
|
||||
note?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
appName: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export default class DeployCmd extends Command {
|
||||
public static description = `\
|
||||
Deploy a single image or a multicontainer project to a balena application.
|
||||
Deploy a single image or a multicontainer project to a balena fleet.
|
||||
|
||||
Usage: \`deploy <appName> ([image] | --build [--source build-dir])\`
|
||||
Usage: \`deploy <fleet> ([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
|
||||
Use this command to deploy an image or a complete multicontainer project to a
|
||||
fleet, 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.)
|
||||
@ -68,13 +80,15 @@ 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.
|
||||
building it first if an image for it doesn't exist. Image names will be looked
|
||||
up according to the scheme: \`<projectName>_<serviceName>\`.
|
||||
|
||||
To deploy to an app on which you're a collaborator, use
|
||||
\`balena deploy <appOwnerUsername>/<appName>\`.
|
||||
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 a fleet where you are a collaborator, use fleet slug including the
|
||||
organization: \`balena deploy <organization>/<fleet>\`.
|
||||
|
||||
${registrySecretsHelp}
|
||||
|
||||
@ -82,44 +96,57 @@ ${dockerignoreHelp}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena deploy myApp',
|
||||
'$ balena deploy myApp --build --source myBuildDir/',
|
||||
'$ balena deploy myApp myApp/myImage',
|
||||
'$ balena deploy myFleet',
|
||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
||||
'$ balena deploy myorg/myfleet --builld --source myBuildDir/ --docker-compose my-custom-compose.yml',
|
||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
|
||||
'$ balena deploy myorg/myfleet myRepo/myImage',
|
||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'appName',
|
||||
description: 'the name of the application to deploy to',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
description: 'the image to deploy',
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
image: Args.string({ description: 'the image to deploy' }),
|
||||
};
|
||||
|
||||
public static usage = 'deploy <appName> [image]';
|
||||
public static usage = 'deploy <fleet> [image]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
source: flags.string({
|
||||
public static flags = {
|
||||
source: Flags.string({
|
||||
description:
|
||||
'specify an alternate source directory; default is the working directory',
|
||||
char: 's',
|
||||
}),
|
||||
build: flags.boolean({
|
||||
build: Flags.boolean({
|
||||
description: 'force a rebuild before deploy',
|
||||
char: 'b',
|
||||
}),
|
||||
nologupload: flags.boolean({
|
||||
nologupload: Flags.boolean({
|
||||
description:
|
||||
"don't upload build logs to the dashboard with image (if building)",
|
||||
}),
|
||||
'release-tag': Flags.string({
|
||||
description: stripIndent`
|
||||
Set release tags if the image deployment is successful. Multiple
|
||||
arguments may be provided, alternating tag keys and values (see examples).
|
||||
Hint: Empty values may be specified with "" (bash, cmd.exe) or '""' (PowerShell).
|
||||
`,
|
||||
multiple: true,
|
||||
}),
|
||||
draft: Flags.boolean({
|
||||
description: stripIndent`
|
||||
Deploy the release as a draft. Draft releases are ignored
|
||||
by the 'track latest' release policy but can be used through release pinning.
|
||||
Draft releases can be marked as final through the API. Releases are created
|
||||
as final by default unless this option is given.`,
|
||||
default: false,
|
||||
}),
|
||||
note: Flags.string({ description: 'The notes for this release' }),
|
||||
...composeCliFlags,
|
||||
...dockerCliFlags,
|
||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||
// Revisit this in future release.
|
||||
help: flags.help({}),
|
||||
help: Flags.help({}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -127,18 +154,19 @@ ${dockerignoreHelp}
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeployCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeployCmd);
|
||||
|
||||
// compositions with many services trigger misleading warnings
|
||||
// @ts-ignore editing property that isn't typed but does exist
|
||||
(await import('events')).defaultMaxListeners = 1000;
|
||||
|
||||
const logger = await Command.getLogger();
|
||||
logger.logDebug('Parsing input...');
|
||||
|
||||
const { appName, image } = params;
|
||||
const { fleet, image } = params;
|
||||
|
||||
// Build args are under consideration for removal - warn user
|
||||
if (options.buildArg) {
|
||||
console.log(buildArgDeprecation);
|
||||
}
|
||||
|
||||
if (image != null && options.build) {
|
||||
throw new ExpectedError(
|
||||
@ -148,52 +176,64 @@ ${dockerignoreHelp}
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||
'../utils/compose_ts'
|
||||
'../../utils/compose_ts'
|
||||
);
|
||||
|
||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||
options['release-tag'] ?? [],
|
||||
);
|
||||
|
||||
if (image) {
|
||||
options['registry-secrets'] = await getRegistrySecrets(
|
||||
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
|
||||
sdk,
|
||||
options['registry-secrets'],
|
||||
);
|
||||
} else {
|
||||
const {
|
||||
dockerfilePath,
|
||||
registrySecrets,
|
||||
} = await validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
});
|
||||
const { dockerfilePath, registrySecrets } =
|
||||
await validateProjectDirectory(sdk, {
|
||||
dockerfilePath: options.dockerfile,
|
||||
noParentCheck: options['noparent-check'] || false,
|
||||
projectPath: options.source || '.',
|
||||
registrySecretsPath: options['registry-secrets'],
|
||||
});
|
||||
options.dockerfile = dockerfilePath;
|
||||
options['registry-secrets'] = registrySecrets;
|
||||
(options as FlagsDef)['registry-secrets'] = registrySecrets;
|
||||
}
|
||||
|
||||
const helpers = await import('../utils/helpers');
|
||||
const app = await helpers.getAppWithArch(appName);
|
||||
const helpers = await import('../../utils/helpers');
|
||||
const app = await helpers.getAppWithArch(fleet);
|
||||
|
||||
const dockerUtils = await import('../utils/docker');
|
||||
const dockerUtils = await import('../../utils/docker');
|
||||
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||
dockerUtils.getDocker(options),
|
||||
dockerUtils.generateBuildOpts(options),
|
||||
dockerUtils.generateBuildOpts(options as FlagsDef),
|
||||
compose.generateOpts(options),
|
||||
]);
|
||||
|
||||
await this.deployProject(docker, logger, composeOpts, {
|
||||
const release = await this.deployProject(docker, logger, composeOpts, {
|
||||
app,
|
||||
appName, // may be prefixed by 'owner/', unlike app.app_name
|
||||
appName: fleet, // may be prefixed by 'owner/', unlike app.app_name
|
||||
image,
|
||||
shouldPerformBuild: !!options.build,
|
||||
shouldUploadLogs: !options.nologupload,
|
||||
buildEmulated: !!options.emulated,
|
||||
createAsDraft: options.draft,
|
||||
buildOpts,
|
||||
});
|
||||
await applyReleaseTagKeysAndValues(
|
||||
sdk,
|
||||
release.id,
|
||||
releaseTagKeys,
|
||||
releaseTagValues,
|
||||
);
|
||||
if (options.note) {
|
||||
await sdk.models.release.setNote(release.id, options.note);
|
||||
}
|
||||
}
|
||||
|
||||
async deployProject(
|
||||
docker: import('docker-toolbelt'),
|
||||
logger: import('../utils/logger'),
|
||||
docker: import('dockerode'),
|
||||
logger: import('../../utils/logger'),
|
||||
composeOpts: ComposeOpts,
|
||||
opts: {
|
||||
app: ApplicationWithArch; // the application instance to deploy to
|
||||
@ -203,23 +243,29 @@ ${dockerignoreHelp}
|
||||
shouldPerformBuild: boolean;
|
||||
shouldUploadLogs: boolean;
|
||||
buildEmulated: boolean;
|
||||
buildOpts: any; // arguments to forward to docker build command
|
||||
buildOpts: BuildOpts;
|
||||
createAsDraft: boolean;
|
||||
},
|
||||
) {
|
||||
const _ = await import('lodash');
|
||||
const doodles = await import('resin-doodles');
|
||||
const sdk = getBalenaSdk();
|
||||
const { deployProject: $deployProject, loadProject } = await import(
|
||||
'../utils/compose_ts'
|
||||
'../../utils/compose_ts'
|
||||
);
|
||||
|
||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
||||
const appType = opts.app.application_type[0];
|
||||
|
||||
try {
|
||||
const project = await loadProject(logger, composeOpts, opts.image);
|
||||
const project = await loadProject(
|
||||
logger,
|
||||
composeOpts,
|
||||
opts.image,
|
||||
opts.buildOpts.t,
|
||||
);
|
||||
if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
|
||||
throw new ExpectedError(
|
||||
'Target application does not support multiple containers. Aborting!',
|
||||
'Target fleet does not support multiple containers. Aborting!',
|
||||
);
|
||||
}
|
||||
|
||||
@ -265,13 +311,12 @@ ${dockerignoreHelp}
|
||||
projectName: project.name,
|
||||
composition: compositionToBuild,
|
||||
arch: opts.app.arch,
|
||||
deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
|
||||
deviceType: opts.app.is_for__device_type[0].slug,
|
||||
emulated: opts.buildEmulated,
|
||||
buildOpts: opts.buildOpts,
|
||||
inlineLogs: composeOpts.inlineLogs,
|
||||
convertEol: composeOpts.convertEol,
|
||||
dockerfilePath: composeOpts.dockerfilePath,
|
||||
nogitignore: composeOpts.nogitignore,
|
||||
multiDockerignore: composeOpts.multiDockerignore,
|
||||
});
|
||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||
@ -286,18 +331,18 @@ ${dockerignoreHelp}
|
||||
},
|
||||
);
|
||||
|
||||
let release;
|
||||
if (appType?.is_legacy) {
|
||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
||||
let release: Release | ComposeReleaseInfo['release'];
|
||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||
const { deployLegacy } = require('../../utils/deploy-legacy');
|
||||
|
||||
const msg = getChalk().yellow(
|
||||
'Target application requires legacy deploy method.',
|
||||
'Target fleet requires legacy deploy method.',
|
||||
);
|
||||
logger.logWarn(msg);
|
||||
|
||||
const [token, username, url, options] = await Promise.all([
|
||||
const [token, { username }, url, options] = await Promise.all([
|
||||
sdk.auth.getToken(),
|
||||
sdk.auth.whoami(),
|
||||
sdk.auth.getUserInfo(),
|
||||
sdk.settings.get('balenaUrl'),
|
||||
{
|
||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||
@ -320,8 +365,8 @@ ${dockerignoreHelp}
|
||||
$select: ['commit'],
|
||||
});
|
||||
} else {
|
||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserId(),
|
||||
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
|
||||
sdk.auth.getUserInfo(),
|
||||
sdk.auth.getToken(),
|
||||
sdk.settings.get('apiUrl'),
|
||||
]);
|
||||
@ -335,6 +380,8 @@ ${dockerignoreHelp}
|
||||
`Bearer ${auth}`,
|
||||
apiEndpoint,
|
||||
!opts.shouldUploadLogs,
|
||||
composeOpts.projectPath,
|
||||
opts.createAsDraft,
|
||||
);
|
||||
}
|
||||
|
||||
@ -344,6 +391,7 @@ ${dockerignoreHelp}
|
||||
console.log();
|
||||
console.log(doodles.getDoodle()); // Show charlie
|
||||
console.log();
|
||||
return release;
|
||||
} catch (err) {
|
||||
logger.logError('Deploy failed');
|
||||
throw err;
|
@ -15,21 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceDeactivateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Deactivate a device.
|
||||
@ -44,17 +34,16 @@ export default class DeviceDeactivateCmd extends Command {
|
||||
'$ balena device deactivate 7cf02a6 --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the UUID of the device to be deactivated',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device deactivate <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -62,9 +51,8 @@ export default class DeviceDeactivateCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceDeactivateCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceDeactivateCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
|
@ -15,22 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } 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.
|
||||
@ -39,25 +29,23 @@ export default class DeviceIdentifyCmd extends Command {
|
||||
`;
|
||||
public static examples = ['$ balena device identify 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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> = {
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
||||
const { args: params } = await this.parse(DeviceIdentifyCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
|
@ -15,18 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { IArg } from '@oclif/parser/lib/args';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { jsonInfo } from '../../utils/messages';
|
||||
|
||||
import type { Application, Release } from 'balena-sdk';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string;
|
||||
fleet: string; // 'org/name' slug
|
||||
device_type?: string;
|
||||
commit?: string;
|
||||
last_seen?: string;
|
||||
@ -41,78 +41,101 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
undervoltage_detected?: boolean;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
${jsonInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device 7cf02a6',
|
||||
'$ balena device 7cf02a6 --view',
|
||||
'$ balena device 7cf02a6 --json',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the device uuid',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
view: Flags.boolean({
|
||||
default: false,
|
||||
description: 'open device dashboard page',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceCmd);
|
||||
const { args: params, flags: options } = await this.parse(DeviceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device = (await balena.models.device.get(params.uuid, {
|
||||
$select: [
|
||||
'device_name',
|
||||
'id',
|
||||
'overall_status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'last_connectivity_event',
|
||||
'uuid',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'memory_usage',
|
||||
'memory_total',
|
||||
'storage_block_device',
|
||||
'storage_usage',
|
||||
'storage_total',
|
||||
'cpu_usage',
|
||||
'cpu_temp',
|
||||
'cpu_id',
|
||||
'is_undervolted',
|
||||
],
|
||||
...expandForAppName,
|
||||
})) as ExtendedDevice;
|
||||
const device = (await balena.models.device.get(
|
||||
params.uuid,
|
||||
options.json
|
||||
? {
|
||||
$expand: {
|
||||
device_tag: {
|
||||
$select: ['tag_key', 'value'],
|
||||
},
|
||||
...expandForAppName.$expand,
|
||||
},
|
||||
}
|
||||
: {
|
||||
$select: [
|
||||
'device_name',
|
||||
'id',
|
||||
'overall_status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'mac_address',
|
||||
'last_connectivity_event',
|
||||
'uuid',
|
||||
'supervisor_version',
|
||||
'is_web_accessible',
|
||||
'note',
|
||||
'os_version',
|
||||
'memory_usage',
|
||||
'memory_total',
|
||||
'public_address',
|
||||
'storage_block_device',
|
||||
'storage_usage',
|
||||
'storage_total',
|
||||
'cpu_usage',
|
||||
'cpu_temp',
|
||||
'cpu_id',
|
||||
'is_undervolted',
|
||||
],
|
||||
...expandForAppName,
|
||||
},
|
||||
)) as ExtendedDevice;
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
const dashboardUrl = balena.models.device.getDashboardUrl(device.uuid);
|
||||
await open(dashboardUrl, { wait: false });
|
||||
return;
|
||||
}
|
||||
|
||||
device.status = device.overall_status;
|
||||
|
||||
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
|
||||
const belongsToApplication =
|
||||
device.belongs_to__application as Application[];
|
||||
device.fleet = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].slug
|
||||
: 'N/a';
|
||||
|
||||
device.device_type = device.is_of__device_type[0].slug;
|
||||
@ -160,6 +183,11 @@ export default class DeviceCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(device, null, 4));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.vertical(device, [
|
||||
`$${device.device_name}$`,
|
||||
@ -168,8 +196,9 @@ export default class DeviceCmd extends Command {
|
||||
'status',
|
||||
'is_online',
|
||||
'ip_address',
|
||||
'public_address',
|
||||
'mac_address',
|
||||
'application_name',
|
||||
'fleet',
|
||||
'last_seen',
|
||||
'uuid',
|
||||
'commit',
|
||||
|
@ -15,49 +15,75 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
import { runCommand } from '../../utils/helpers';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
fleet?: string;
|
||||
yes: boolean;
|
||||
advanced: boolean;
|
||||
'os-version'?: string;
|
||||
drive?: string;
|
||||
config?: string;
|
||||
help: void;
|
||||
'provisioning-key-name'?: string;
|
||||
'provisioning-key-expiry-date'?: string;
|
||||
}
|
||||
|
||||
export default class DeviceInitCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Initialise a device with balenaOS.
|
||||
Initialize a device with balenaOS.
|
||||
|
||||
Initialise a device by downloading the OS image of a certain application
|
||||
and writing it to an SD Card.
|
||||
Register a new device in the selected fleet, download the OS image for the
|
||||
fleet's default device type, configure the image and write it to an SD card.
|
||||
This command effectively combines several other balena CLI commands in one,
|
||||
namely:
|
||||
|
||||
'balena device register'
|
||||
'balena os download'
|
||||
'balena os build-config' or 'balena config generate'
|
||||
'balena os configure'
|
||||
'balena os local flash'
|
||||
|
||||
Possible arguments for the '--fleet', '--os-version' and '--drive' options can
|
||||
be listed respectively with the commands:
|
||||
|
||||
'balena fleets'
|
||||
'balena os versions'
|
||||
'balena util available-drives'
|
||||
|
||||
If the '--fleet' or '--drive' options are omitted, interactive menus will be
|
||||
presented with values to choose from. If the '--os-version' option is omitted,
|
||||
the latest released OS version for the fleet's default device type will be used.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
Image configuration questions will be asked interactively unless a pre-configured
|
||||
'config.json' file is provided with the '--config' option. The file can be
|
||||
generated with the 'balena config generate' or 'balena os build-config' commands.
|
||||
`;
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device init',
|
||||
'$ balena device init --application MyApp',
|
||||
'$ balena device init -f myorg/myfleet',
|
||||
'$ balena device init --fleet myFleet --os-version 2.101.7 --drive /dev/disk5 --config config.json --yes',
|
||||
'$ balena device init --fleet myFleet --os-version 2.83.21+rev1.prod --drive /dev/disk5 --config config.json --yes',
|
||||
];
|
||||
|
||||
public static usage = 'device init';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
yes: cf.yes,
|
||||
advanced: flags.boolean({
|
||||
advanced: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'show advanced configuration options',
|
||||
}),
|
||||
'os-version': flags.string({
|
||||
'os-version': Flags.string({
|
||||
description: stripIndent`
|
||||
exact version number, or a valid semver range,
|
||||
or 'latest' (includes pre-releases),
|
||||
@ -67,16 +93,23 @@ export default class DeviceInitCmd extends Command {
|
||||
`,
|
||||
}),
|
||||
drive: cf.drive,
|
||||
config: flags.string({
|
||||
config: Flags.string({
|
||||
description: 'path to the config JSON file, see `balena os build-config`',
|
||||
}),
|
||||
'provisioning-key-name': Flags.string({
|
||||
description: 'custom key name assigned to generated provisioning api key',
|
||||
}),
|
||||
'provisioning-key-expiry-date': Flags.string({
|
||||
description:
|
||||
'expiry date assigned to generated provisioning api key (format: YYYY-MM-DD)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
|
||||
const { flags: options } = await this.parse(DeviceInitCmd);
|
||||
|
||||
// Imports
|
||||
const { promisify } = await import('util');
|
||||
@ -90,27 +123,21 @@ export default class DeviceInitCmd extends Command {
|
||||
const logger = await Command.getLogger();
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Get application and
|
||||
const application = (await getApplication(
|
||||
balena,
|
||||
options['application'] ||
|
||||
(await (await import('../../utils/patterns')).selectApplication()),
|
||||
{
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
const application = options.fleet
|
||||
? await getApplication(balena, options.fleet, {
|
||||
$select: ['id', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)) as ApplicationWithDeviceType;
|
||||
})
|
||||
: await (await import('../../utils/patterns')).selectApplication();
|
||||
|
||||
// Register new device
|
||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
||||
console.info(`Registering to ${application.app_name}: ${deviceUuid}`);
|
||||
console.info(`Registering to ${application.slug}: ${deviceUuid}`);
|
||||
await balena.models.device.register(application.id, deviceUuid);
|
||||
const device = await balena.models.device.get(deviceUuid);
|
||||
|
||||
@ -153,6 +180,21 @@ export default class DeviceInitCmd extends Command {
|
||||
} else if (options.advanced) {
|
||||
configureCommand.push('--advanced');
|
||||
}
|
||||
|
||||
if (options['provisioning-key-name']) {
|
||||
configureCommand.push(
|
||||
'--provisioning-key-name',
|
||||
options['provisioning-key-name'],
|
||||
);
|
||||
}
|
||||
|
||||
if (options['provisioning-key-expiry-date']) {
|
||||
configureCommand.push(
|
||||
'--provisioning-key-expiry-date',
|
||||
options['provisioning-key-expiry-date'],
|
||||
);
|
||||
}
|
||||
|
||||
await runCommand(configureCommand);
|
||||
}
|
||||
|
||||
|
98
lib/commands/device/local-mode.ts
Normal file
98
lib/commands/device/local-mode.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class DeviceLocalModeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Get or manage the local mode status for a device.
|
||||
|
||||
Output current local mode status, or enable/disable local mode
|
||||
for specified device.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena device local-mode 23c73a1',
|
||||
'$ balena device local-mode 23c73a1 --enable',
|
||||
'$ balena device local-mode 23c73a1 --disable',
|
||||
'$ balena device local-mode 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to manage',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device local-mode <uuid>';
|
||||
|
||||
public static flags = {
|
||||
enable: Flags.boolean({
|
||||
description: 'enable local mode',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: Flags.boolean({
|
||||
description: 'disable local mode',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: Flags.boolean({
|
||||
description: 'output boolean indicating local mode status',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceLocalModeCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
if (options.enable) {
|
||||
await balena.models.device.enableLocalMode(params.uuid);
|
||||
console.log(`Local mode on device ${params.uuid} is now ENABLED.`);
|
||||
} else if (options.disable) {
|
||||
await balena.models.device.disableLocalMode(params.uuid);
|
||||
console.log(`Local mode on device ${params.uuid} is now DISABLED.`);
|
||||
} else if (options.status) {
|
||||
// Output bool indicating local mode status
|
||||
const isEnabled = await balena.models.device.isInLocalMode(params.uuid);
|
||||
console.log(isEnabled);
|
||||
} else {
|
||||
// If no flag provided, output status and tip
|
||||
const isEnabled = await balena.models.device.isInLocalMode(params.uuid);
|
||||
console.log(
|
||||
`Local mode on device ${params.uuid} is ${
|
||||
isEnabled ? 'ENABLED' : 'DISABLED'
|
||||
}.`,
|
||||
);
|
||||
if (isEnabled) {
|
||||
console.log('To disable, use:');
|
||||
console.log(` balena device local-mode ${params.uuid} --disable`);
|
||||
} else {
|
||||
console.log('To enable, use:');
|
||||
console.log(` balena device local-mode ${params.uuid} --enable`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -15,107 +15,104 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import type { Application, BalenaSDK } from 'balena-sdk';
|
||||
import { Args } from '@oclif/core';
|
||||
import type {
|
||||
BalenaSDK,
|
||||
Device,
|
||||
PineOptions,
|
||||
PineTypedResult,
|
||||
} from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { ExpectedError } from '../../errors';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
application_name?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class DeviceMoveCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Move one or more devices to another application.
|
||||
Move one or more devices to another fleet.
|
||||
|
||||
Move one or more devices to another application.
|
||||
Move one or more devices to another fleet.
|
||||
|
||||
If --fleet is omitted, the fleet will be prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
Note, if the application option is omitted it will be prompted
|
||||
for interactively.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device move 7cf02a6',
|
||||
'$ balena device move 7cf02a6,dc39e52',
|
||||
'$ balena device move 7cf02a6 --application MyNewApp',
|
||||
'$ balena device move 7cf02a6 --fleet MyNewFleet',
|
||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device move <uuid(s)>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceMoveCmd,
|
||||
);
|
||||
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
|
||||
const deviceOptions = {
|
||||
$select: 'belongs_to__application',
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'is_of__cpu_architecture',
|
||||
$expand: {
|
||||
is_of__cpu_architecture: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
|
||||
// Parse ids string into array of correct types
|
||||
const deviceIds: Array<string | number> = params.uuid
|
||||
.split(',')
|
||||
.map((id) => tryAsInteger(id));
|
||||
|
||||
// Get devices
|
||||
// TODO: Refacor once `device.get()` accepts an array of uuids`
|
||||
const devices = await Promise.all(
|
||||
deviceIds.map(
|
||||
deviceUuids.map(
|
||||
(uuid) =>
|
||||
balena.models.device.get(uuid, expandForAppName) as Promise<
|
||||
ExtendedDevice
|
||||
balena.models.device.get(uuid, deviceOptions) as Promise<
|
||||
PineTypedResult<Device, typeof deviceOptions>
|
||||
>,
|
||||
),
|
||||
);
|
||||
return devices;
|
||||
}
|
||||
|
||||
// Map application name for each device
|
||||
for (const device of devices) {
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]
|
||||
? belongsToApplication[0].app_name
|
||||
: 'N/a';
|
||||
}
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(DeviceMoveCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Split uuids string into array of uuids
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
|
||||
const devices = await this.getDevices(balena, deviceUuids);
|
||||
|
||||
// Disambiguate application
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
// Get destination application
|
||||
const application =
|
||||
options.application ||
|
||||
(await this.interactivelySelectApplication(balena, devices));
|
||||
const application = options.fleet
|
||||
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
|
||||
: await this.interactivelySelectApplication(balena, devices);
|
||||
|
||||
// Move each device
|
||||
for (const uuid of deviceIds) {
|
||||
for (const uuid of deviceUuids) {
|
||||
try {
|
||||
await balena.models.device.move(uuid, tryAsInteger(application));
|
||||
console.info(`${uuid} was moved to ${application}`);
|
||||
await balena.models.device.move(uuid, application.id);
|
||||
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
||||
@ -125,45 +122,55 @@ export default class DeviceMoveCmd extends Command {
|
||||
|
||||
async interactivelySelectApplication(
|
||||
balena: BalenaSDK,
|
||||
devices: ExtendedDevice[],
|
||||
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
||||
) {
|
||||
const [deviceDeviceTypes, deviceTypes] = await Promise.all([
|
||||
Promise.all(
|
||||
devices.map((device) =>
|
||||
balena.models.device.getManifestBySlug(
|
||||
device.is_of__device_type[0].slug,
|
||||
),
|
||||
// deduplicate the slugs
|
||||
const deviceCpuArchs = Array.from(
|
||||
new Set(
|
||||
devices.map(
|
||||
(d) => d.is_of__device_type[0].is_of__cpu_architecture[0].slug,
|
||||
),
|
||||
),
|
||||
balena.models.config.getDeviceTypes(),
|
||||
]);
|
||||
|
||||
const compatibleDeviceTypes = deviceTypes.filter((dt) =>
|
||||
deviceDeviceTypes.every(
|
||||
(deviceDeviceType) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceDeviceType.arch,
|
||||
dt.arch,
|
||||
) &&
|
||||
!!dt.isDependent === !!deviceDeviceType.isDependent &&
|
||||
dt.state !== 'DISCONTINUED',
|
||||
),
|
||||
);
|
||||
|
||||
const allCpuArches = await balena.pine.get({
|
||||
resource: 'cpu_architecture',
|
||||
options: {
|
||||
$select: ['id', 'slug'],
|
||||
},
|
||||
});
|
||||
|
||||
const compatibleCpuArchIds = allCpuArches
|
||||
.filter((cpuArch) => {
|
||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||
balena.models.os.isArchitectureCompatibleWith(
|
||||
deviceCpuArch,
|
||||
cpuArch.slug,
|
||||
),
|
||||
);
|
||||
})
|
||||
.map((deviceType) => deviceType.id);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
try {
|
||||
const application = await patterns.selectApplication(
|
||||
(app) =>
|
||||
compatibleDeviceTypes.some(
|
||||
(dt) => dt.slug === app.is_for__device_type[0].slug,
|
||||
) &&
|
||||
// @ts-ignore using the extended device object prop
|
||||
devices.some((device) => device.application_name !== app.app_name),
|
||||
{
|
||||
is_for__device_type: {
|
||||
$any: {
|
||||
$alias: 'dt',
|
||||
$expr: {
|
||||
dt: {
|
||||
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
return application;
|
||||
} catch (err) {
|
||||
if (deviceDeviceTypes.length) {
|
||||
if (!compatibleCpuArchIds.length) {
|
||||
throw new ExpectedError(
|
||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||
);
|
||||
|
@ -15,25 +15,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } 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.
|
||||
@ -47,23 +35,29 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device os-update 23c73a1',
|
||||
'$ balena device os-update 23c73a1 --version 2.101.7',
|
||||
'$ balena device os-update 23c73a1 --version 2.31.0+rev1.prod',
|
||||
'$ balena device os-update 23c73a1 --include-draft',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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({
|
||||
public static flags = {
|
||||
version: Flags.string({
|
||||
description: 'a balenaOS version',
|
||||
exclusive: ['include-draft'],
|
||||
}),
|
||||
'include-draft': Flags.boolean({
|
||||
description: 'include pre-release balenaOS versions',
|
||||
default: false,
|
||||
exclusive: ['version'],
|
||||
}),
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
@ -72,26 +66,21 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceOsUpdateCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceOsUpdateCmd);
|
||||
|
||||
const sdk = getBalenaSdk();
|
||||
|
||||
// Get device info
|
||||
const {
|
||||
uuid,
|
||||
is_of__device_type,
|
||||
os_version,
|
||||
os_variant,
|
||||
} = (await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'os_version', 'os_variant'],
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'slug',
|
||||
const { uuid, is_of__device_type, os_version, os_variant } =
|
||||
(await sdk.models.device.get(params.uuid, {
|
||||
$select: ['uuid', 'os_version', 'os_variant'],
|
||||
$expand: {
|
||||
is_of__device_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as DeviceWithDeviceType;
|
||||
})) as DeviceWithDeviceType;
|
||||
|
||||
// Get current device OS version
|
||||
const currentOsVersion = sdk.models.device.getOsVersion({
|
||||
@ -104,10 +93,25 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
);
|
||||
}
|
||||
|
||||
let includeDraft = options['include-draft'];
|
||||
if (!includeDraft && options.version != null) {
|
||||
const bSemver = await import('balena-semver');
|
||||
const parsedVersion = bSemver.parse(options.version);
|
||||
// When the user provides a draft version, we need to pass `includeDraft`
|
||||
// to the os.getSupportedOsUpdateVersions() since w/o it the results
|
||||
// will for sure not include the user provided version and the command
|
||||
// would return a "not in the Host OS update targets" error.
|
||||
includeDraft =
|
||||
parsedVersion != null && parsedVersion.prerelease.length > 0;
|
||||
}
|
||||
|
||||
// Get supported OS update versions
|
||||
const hupVersionInfo = await sdk.models.os.getSupportedOsUpdateVersions(
|
||||
is_of__device_type[0].slug,
|
||||
currentOsVersion,
|
||||
{
|
||||
includeDraft,
|
||||
},
|
||||
);
|
||||
if (hupVersionInfo.versions.length === 0) {
|
||||
throw new ExpectedError(
|
||||
@ -118,6 +122,8 @@ export default class DeviceOsUpdateCmd extends Command {
|
||||
// Get target OS version
|
||||
let targetOsVersion = options.version;
|
||||
if (targetOsVersion != null) {
|
||||
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||
targetOsVersion = normalizeOsVersion(targetOsVersion);
|
||||
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
||||
throw new ExpectedError(
|
||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
||||
|
91
lib/commands/device/pin.ts
Normal file
91
lib/commands/device/pin.ts
Normal file
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
export default class DevicePinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Pin a device to a release.
|
||||
|
||||
Pin a device to a release.
|
||||
|
||||
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device pin 7cf02a6',
|
||||
'$ balena device pin 7cf02a6 91165e5',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to pin to a release',
|
||||
required: true,
|
||||
}),
|
||||
releaseToPinTo: Args.string({
|
||||
description: 'the commit of the release for the device to get pinned to',
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device pin <uuid> [releaseToPinTo]';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(DevicePinCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const device = await balena.models.device.get(params.uuid, {
|
||||
$expand: {
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
belongs_to__application: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pinnedRelease = getExpandedProp(
|
||||
device.should_be_running__release,
|
||||
'commit',
|
||||
);
|
||||
const appSlug = getExpandedProp(device.belongs_to__application, 'slug');
|
||||
|
||||
const releaseToPinTo = params.releaseToPinTo;
|
||||
|
||||
if (!releaseToPinTo) {
|
||||
console.log(
|
||||
`${
|
||||
pinnedRelease
|
||||
? `This device is currently pinned to ${pinnedRelease}.`
|
||||
: 'This device is not currently pinned to any release.'
|
||||
} \n\nTo see a list of all releases this device can be pinned to, run \`balena releases ${appSlug}\`.`,
|
||||
);
|
||||
} else {
|
||||
await balena.models.device.pinToRelease(params.uuid, releaseToPinTo);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,26 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } 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`
|
||||
@ -43,9 +28,6 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
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 = [
|
||||
@ -55,33 +37,25 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
'$ balena device public-url 23c73a1 --status',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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({
|
||||
public static flags = {
|
||||
enable: Flags.boolean({
|
||||
description: 'enable the public URL',
|
||||
exclusive: ['disable', 'status'],
|
||||
}),
|
||||
disable: flags.boolean({
|
||||
disable: Flags.boolean({
|
||||
description: 'disable the public URL',
|
||||
exclusive: ['enable', 'status'],
|
||||
}),
|
||||
status: flags.boolean({
|
||||
status: Flags.boolean({
|
||||
description: 'determine if public URL is enabled',
|
||||
exclusive: ['enable', 'disable'],
|
||||
}),
|
||||
@ -91,28 +65,8 @@ export default class DevicePublicUrlCmd extends Command {
|
||||
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 { args: params, flags: options } =
|
||||
await this.parse(DevicePublicUrlCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
|
@ -15,26 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DevicePurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge application data from a device.
|
||||
Purge data from a device.
|
||||
|
||||
Purge application data from a device.
|
||||
This will clear the application's /data directory.
|
||||
Purge data from a device.
|
||||
This will clear the device's "/data" directory.
|
||||
|
||||
Multiple devices may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
@ -46,34 +37,30 @@ export default class DevicePurgeCmd extends Command {
|
||||
|
||||
public static usage = 'device purge <uuid>';
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DevicePurgeCmd);
|
||||
const { args: params } = await this.parse(DevicePurgeCmd);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Purging data from device ${deviceId}`);
|
||||
await balena.models.device.purge(deviceId);
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Purging data from device ${uuid}`);
|
||||
await balena.models.device.purge(uuid);
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } 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`
|
||||
@ -39,18 +28,16 @@ export default class DeviceRebootCmd extends Command {
|
||||
`;
|
||||
public static examples = ['$ balena device reboot 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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> = {
|
||||
public static flags = {
|
||||
force: cf.force,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -58,9 +45,7 @@ export default class DeviceRebootCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRebootCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceRebootCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
|
@ -15,67 +15,71 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface FlagsDef {
|
||||
uuid?: string;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
application: string;
|
||||
}
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class DeviceRegisterCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Register a device.
|
||||
Register a new device.
|
||||
|
||||
Register a new device with a balena fleet.
|
||||
|
||||
If --uuid is not provided, a new UUID will be automatically assigned.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
Register a device to an application.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device register MyApp',
|
||||
'$ balena device register MyApp --uuid <uuid>',
|
||||
'$ balena device register MyFleet',
|
||||
'$ balena device register MyFleet --uuid <uuid>',
|
||||
'$ balena device register myorg/myfleet --uuid <uuid>',
|
||||
'$ balena device register myorg/myfleet --uuid <uuid> --deviceType <deviceTypeSlug>',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'application',
|
||||
description: 'the name or id of application to register device with',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
};
|
||||
|
||||
public static usage = 'device register <application>';
|
||||
public static usage = 'device register <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
uuid: flags.string({
|
||||
public static flags = {
|
||||
uuid: Flags.string({
|
||||
description: 'custom uuid',
|
||||
char: 'u',
|
||||
}),
|
||||
deviceType: Flags.string({
|
||||
description:
|
||||
"device type slug (run 'balena devices supported' for possible values)",
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRegisterCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceRegisterCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = await getApplication(balena, params.application);
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$select: ['id', 'slug'],
|
||||
});
|
||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
||||
|
||||
console.info(`Registering to ${application.app_name}: ${uuid}`);
|
||||
console.info(`Registering to ${application.slug}: ${uuid}`);
|
||||
|
||||
const result = await balena.models.device.register(application.id, uuid);
|
||||
const result = await balena.models.device.register(
|
||||
application.id,
|
||||
uuid,
|
||||
options.deviceType,
|
||||
);
|
||||
|
||||
return result && result.uuid;
|
||||
}
|
||||
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } 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`
|
||||
@ -44,29 +33,26 @@ export default class DeviceRenameCmd extends Command {
|
||||
'$ balena device rename 7cf02a6 MyPi',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'the uuid of the device to rename',
|
||||
parse: (dev) => tryAsInteger(dev),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'newName',
|
||||
}),
|
||||
newName: Args.string({
|
||||
description: 'the new name for the device',
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device rename <uuid> [newName]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
|
||||
const { args: params } = await this.parse(DeviceRenameCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
|
@ -15,8 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
@ -26,15 +25,6 @@ import type {
|
||||
CurrentServiceWithCommit,
|
||||
} from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
service?: string;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
uuid: string;
|
||||
}
|
||||
|
||||
export default class DeviceRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart containers on a device.
|
||||
@ -55,19 +45,18 @@ export default class DeviceRestartCmd extends Command {
|
||||
'$ balena device restart 23c73a1 -s myService1,myService2',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device restart <uuid>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
service: flags.string({
|
||||
public static flags = {
|
||||
service: Flags.string({
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of service names to restart',
|
||||
char: 's',
|
||||
@ -78,28 +67,23 @@ export default class DeviceRestartCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRestartCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceRestartCmd);
|
||||
|
||||
const { tryAsInteger } = await import('../../utils/validation');
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceIds = params.uuid.split(',').map((id) => {
|
||||
return tryAsInteger(id);
|
||||
});
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
const serviceNames = options.service?.split(',');
|
||||
|
||||
// Iterate sequentially through deviceIds.
|
||||
// Iterate sequentially through deviceUuids.
|
||||
// We may later want to add a batching feature,
|
||||
// so that n devices are processed in parallel
|
||||
for (const deviceId of deviceIds) {
|
||||
ux.action.start(`Restarting services on device ${deviceId}`);
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Restarting services on device ${uuid}`);
|
||||
if (serviceNames) {
|
||||
await this.restartServices(balena, deviceId, serviceNames);
|
||||
await this.restartServices(balena, uuid, serviceNames);
|
||||
} else {
|
||||
await this.restartAllServices(balena, deviceId);
|
||||
await this.restartAllServices(balena, uuid);
|
||||
}
|
||||
ux.action.stop();
|
||||
}
|
||||
@ -107,7 +91,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
|
||||
async restartServices(
|
||||
balena: BalenaSDK,
|
||||
deviceId: number | string,
|
||||
deviceUuid: string,
|
||||
serviceNames: string[],
|
||||
) {
|
||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||
@ -116,7 +100,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
// Get device
|
||||
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
||||
try {
|
||||
device = await balena.models.device.getWithServiceDetails(deviceId, {
|
||||
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
|
||||
$expand: {
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
@ -124,7 +108,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
@ -136,7 +120,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
serviceNames.forEach((service) => {
|
||||
if (!device.current_services[service]) {
|
||||
throw new ExpectedError(
|
||||
`Service ${service} not found on device ${deviceId}.`,
|
||||
`Service ${service} not found on device ${deviceUuid}.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -155,7 +139,7 @@ export default class DeviceRestartCmd extends Command {
|
||||
if (serviceContainer) {
|
||||
restartPromises.push(
|
||||
balena.models.device.restartService(
|
||||
deviceId,
|
||||
deviceUuid,
|
||||
serviceContainer.image_id,
|
||||
),
|
||||
);
|
||||
@ -166,32 +150,32 @@ export default class DeviceRestartCmd extends Command {
|
||||
await Promise.all(restartPromises);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device')) {
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async restartAllServices(balena: BalenaSDK, deviceId: number | string) {
|
||||
async restartAllServices(balena: BalenaSDK, deviceUuid: string) {
|
||||
// Note: device.restartApplication throws `BalenaDeviceNotFound: Device not found` if device not online.
|
||||
// Need to use device.get first to distinguish between non-existant and offline devices.
|
||||
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||
try {
|
||||
const device = await balena.models.device.get(deviceId);
|
||||
const device = await balena.models.device.get(deviceUuid);
|
||||
if (!device.is_online) {
|
||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
}
|
||||
} catch (e) {
|
||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
||||
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await balena.models.device.restartApplication(deviceId);
|
||||
await balena.models.device.restartApplication(deviceUuid);
|
||||
}
|
||||
}
|
||||
|
@ -15,21 +15,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } 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`
|
||||
@ -46,18 +35,17 @@ export default class DeviceRmCmd extends Command {
|
||||
'$ balena device rm 7cf02a6 --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description:
|
||||
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device rm <uuid(s)>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -65,9 +53,7 @@ export default class DeviceRmCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceRmCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(DeviceRmCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const patterns = await import('../../utils/patterns');
|
||||
@ -84,7 +70,7 @@ export default class DeviceRmCmd extends Command {
|
||||
// Remove
|
||||
for (const uuid of uuids) {
|
||||
try {
|
||||
await balena.models.device.remove(tryAsInteger(uuid));
|
||||
await balena.models.device.remove(uuid);
|
||||
} catch (err) {
|
||||
console.info(`${err.message}, uuid: ${uuid}`);
|
||||
process.exitCode = 1;
|
||||
|
@ -15,23 +15,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { IArg } from '@oclif/parser/lib/args';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } 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.
|
||||
@ -40,18 +29,16 @@ export default class DeviceShutdownCmd extends Command {
|
||||
`;
|
||||
public static examples = ['$ balena device shutdown 23c73a1'];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'uuid',
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
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> = {
|
||||
public static flags = {
|
||||
force: cf.force,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -59,9 +46,8 @@ export default class DeviceShutdownCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
DeviceShutdownCmd,
|
||||
);
|
||||
const { args: params, flags: options } =
|
||||
await this.parse(DeviceShutdownCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
|
139
lib/commands/device/start-service.ts
Normal file
139
lib/commands/device/start-service.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
import type { BalenaSDK } from 'balena-sdk';
|
||||
|
||||
export default class DeviceStartServiceCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Start containers on a device.
|
||||
|
||||
Start containers on a device.
|
||||
|
||||
Multiple devices and services may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device start-service 23c73a1 myService',
|
||||
'$ balena device start-service 23c73a1 myService1,myService2',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
}),
|
||||
service: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of service names',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device start-service <uuid>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(DeviceStartServiceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
const serviceNames = params.service.split(',');
|
||||
|
||||
// Iterate sequentially through deviceUuids.
|
||||
// We may later want to add a batching feature,
|
||||
// so that n devices are processed in parallel
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Starting services on device ${uuid}`);
|
||||
await this.startServices(balena, uuid, serviceNames);
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async startServices(
|
||||
balena: BalenaSDK,
|
||||
deviceUuid: string,
|
||||
serviceNames: string[],
|
||||
) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
|
||||
// Get device
|
||||
const device = await balena.models.device.getWithServiceDetails(
|
||||
deviceUuid,
|
||||
{
|
||||
$expand: {
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const activeReleaseCommit = getExpandedProp(
|
||||
device.is_running__release,
|
||||
'commit',
|
||||
);
|
||||
|
||||
// Check specified services exist on this device before startinganything
|
||||
serviceNames.forEach((service) => {
|
||||
if (!device.current_services[service]) {
|
||||
throw new ExpectedError(
|
||||
`Service ${service} not found on device ${deviceUuid}.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Start services
|
||||
const startPromises: Array<Promise<void>> = [];
|
||||
for (const serviceName of serviceNames) {
|
||||
const service = device.current_services[serviceName];
|
||||
// Each service is an array of `CurrentServiceWithCommit`
|
||||
// because when service is updating, it will actually hold 2 services
|
||||
// Target commit matching `device.is_running__release`
|
||||
const serviceContainer = service.find((s) => {
|
||||
return s.commit === activeReleaseCommit;
|
||||
});
|
||||
|
||||
if (serviceContainer) {
|
||||
startPromises.push(
|
||||
balena.models.device.startService(
|
||||
deviceUuid,
|
||||
serviceContainer.image_id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(startPromises);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device')) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
139
lib/commands/device/stop-service.ts
Normal file
139
lib/commands/device/stop-service.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||
import type { BalenaSDK } from 'balena-sdk';
|
||||
|
||||
export default class DeviceStopServiceCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Stop containers on a device.
|
||||
|
||||
Stop containers on a device.
|
||||
|
||||
Multiple devices and services may be specified with a comma-separated list
|
||||
of values (no spaces).
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena device stop-service 23c73a1 myService',
|
||||
'$ balena device stop-service 23c73a1 myService1,myService2',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||
required: true,
|
||||
}),
|
||||
service: Args.string({
|
||||
description: 'comma-separated list (no blank spaces) of service names',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device stop-service <uuid>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(DeviceStopServiceCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const ux = getCliUx();
|
||||
|
||||
const deviceUuids = params.uuid.split(',');
|
||||
const serviceNames = params.service.split(',');
|
||||
|
||||
// Iterate sequentially through deviceUuids.
|
||||
// We may later want to add a batching feature,
|
||||
// so that n devices are processed in parallel
|
||||
for (const uuid of deviceUuids) {
|
||||
ux.action.start(`Stopping services on device ${uuid}`);
|
||||
await this.stopServices(balena, uuid, serviceNames);
|
||||
ux.action.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async stopServices(
|
||||
balena: BalenaSDK,
|
||||
deviceUuid: string,
|
||||
serviceNames: string[],
|
||||
) {
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
const { getExpandedProp } = await import('../../utils/pine');
|
||||
|
||||
// Get device
|
||||
const device = await balena.models.device.getWithServiceDetails(
|
||||
deviceUuid,
|
||||
{
|
||||
$expand: {
|
||||
is_running__release: { $select: 'commit' },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const activeReleaseCommit = getExpandedProp(
|
||||
device.is_running__release,
|
||||
'commit',
|
||||
);
|
||||
|
||||
// Check specified services exist on this device before stoppinganything
|
||||
serviceNames.forEach((service) => {
|
||||
if (!device.current_services[service]) {
|
||||
throw new ExpectedError(
|
||||
`Service ${service} not found on device ${deviceUuid}.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Stop services
|
||||
const stopPromises: Array<Promise<void>> = [];
|
||||
for (const serviceName of serviceNames) {
|
||||
const service = device.current_services[serviceName];
|
||||
// Each service is an array of `CurrentServiceWithCommit`
|
||||
// because when service is updating, it will actually hold 2 services
|
||||
// Target commit matching `device.is_running__release`
|
||||
const serviceContainer = service.find((s) => {
|
||||
return s.commit === activeReleaseCommit;
|
||||
});
|
||||
|
||||
if (serviceContainer) {
|
||||
stopPromises.push(
|
||||
balena.models.device.stopService(
|
||||
deviceUuid,
|
||||
serviceContainer.image_id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(stopPromises);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device')) {
|
||||
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
lib/commands/device/track-fleet.ts
Normal file
53
lib/commands/device/track-fleet.ts
Normal file
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class DeviceTrackFleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Make a device track the fleet's pinned release.
|
||||
|
||||
Make a device track the fleet's pinned release.
|
||||
`;
|
||||
public static examples = ['$ balena device track-fleet 7cf02a6'];
|
||||
|
||||
public static args = {
|
||||
uuid: Args.string({
|
||||
description: "the uuid of the device to make track the fleet's release",
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'device track-fleet <uuid>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(DeviceTrackFleetCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await balena.models.device.trackApplicationRelease(params.uuid);
|
||||
}
|
||||
}
|
@ -15,58 +15,50 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { expandForAppName } from '../../utils/helpers';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import type { Application } from 'balena-sdk';
|
||||
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||
|
||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||
dashboard_url?: string;
|
||||
application_name?: string | null;
|
||||
device_type?: string | null;
|
||||
}
|
||||
import type { Device, PineOptions } from 'balena-sdk';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string;
|
||||
app?: string;
|
||||
help: void;
|
||||
json: boolean;
|
||||
}
|
||||
const devicesSelectFields = {
|
||||
$select: [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
],
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
export default class DevicesCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all devices.
|
||||
|
||||
list all devices that belong to you.
|
||||
List all of your devices.
|
||||
|
||||
You can filter the devices by application by using the \`--application\` option.
|
||||
Devices can be filtered by fleet with the \`--fleet\` option.
|
||||
|
||||
The --json option is recommended when scripting the output of this command,
|
||||
because field names are less likely to change in JSON format and because it
|
||||
better represents data types like arrays, empty strings and null values.
|
||||
The 'jq' utility may be helpful for querying JSON fields in shell scripts
|
||||
(https://stedolan.github.io/jq/manual/).
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
|
||||
${jsonInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices',
|
||||
'$ balena devices --application MyApp',
|
||||
'$ balena devices --app MyApp',
|
||||
'$ balena devices -a MyApp',
|
||||
'$ balena devices --fleet MyFleet',
|
||||
'$ balena devices -f myorg/myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'devices';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: cf.application,
|
||||
app: cf.app,
|
||||
public static flags = {
|
||||
fleet: cf.fleet,
|
||||
json: cf.json,
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
};
|
||||
|
||||
public static primary = true;
|
||||
@ -74,61 +66,60 @@ export default class DevicesCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
||||
const { flags: options } = await this.parse(DevicesCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
const devicesOptions = {
|
||||
...devicesSelectFields,
|
||||
...expandForAppName,
|
||||
$orderby: { device_name: 'asc' },
|
||||
} satisfies PineOptions<Device>;
|
||||
|
||||
// Consolidate application options
|
||||
options.application = options.application || options.app;
|
||||
delete options.app;
|
||||
const devices = (
|
||||
await (async () => {
|
||||
if (options.fleet != null) {
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, options.fleet, {
|
||||
$select: 'slug',
|
||||
$expand: {
|
||||
owns__device: devicesOptions,
|
||||
},
|
||||
});
|
||||
return application.owns__device;
|
||||
}
|
||||
|
||||
let devices;
|
||||
return await balena.pine.get({
|
||||
resource: 'device',
|
||||
options: devicesOptions,
|
||||
});
|
||||
})()
|
||||
).map((device) => ({
|
||||
...device,
|
||||
dashboard_url: balena.models.device.getDashboardUrl(device.uuid),
|
||||
fleet: device.belongs_to__application?.[0]?.slug || null,
|
||||
uuid: options.json ? device.uuid : device.uuid.slice(0, 7),
|
||||
device_type: device.is_of__device_type?.[0]?.slug || null,
|
||||
}));
|
||||
|
||||
if (options.application != null) {
|
||||
devices = (await balena.models.device.getAllByApplication(
|
||||
tryAsInteger(options.application),
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
} else {
|
||||
devices = (await balena.models.device.getAll(
|
||||
expandForAppName,
|
||||
)) as ExtendedDevice[];
|
||||
}
|
||||
|
||||
devices = devices.map(function (device) {
|
||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||
|
||||
const belongsToApplication = device.belongs_to__application as Application[];
|
||||
device.application_name = belongsToApplication?.[0]?.app_name || null;
|
||||
|
||||
device.uuid = options.json ? device.uuid : device.uuid.slice(0, 7);
|
||||
|
||||
device.device_type = device.is_of__device_type?.[0]?.slug || null;
|
||||
return device;
|
||||
});
|
||||
|
||||
const fields = [
|
||||
const fields: Array<keyof (typeof devices)[number]> = [
|
||||
'id',
|
||||
'uuid',
|
||||
'device_name',
|
||||
'device_type',
|
||||
'application_name',
|
||||
'fleet',
|
||||
'status',
|
||||
'is_online',
|
||||
'supervisor_version',
|
||||
'os_version',
|
||||
'dashboard_url',
|
||||
];
|
||||
const _ = await import('lodash');
|
||||
|
||||
if (options.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
devices.map((device) => _.pick(device, fields)),
|
||||
null,
|
||||
4,
|
||||
),
|
||||
);
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||
console.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
const _ = await import('lodash');
|
||||
console.log(
|
||||
getVisuals().table.horizontal(
|
||||
devices.map((dev) => _.mapValues(dev, (val) => val ?? 'N/a')),
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,8 +14,8 @@
|
||||
* 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 { Flags } from '@oclif/core';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../../command';
|
||||
|
||||
@ -23,24 +23,12 @@ import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { CommandHelp } from '../../utils/oclif-utils';
|
||||
|
||||
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 'new', '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
|
||||
@ -49,8 +37,7 @@ export default class DevicesSupportedCmd extends Command {
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena devices supported',
|
||||
'$ balena devices supported --verbose',
|
||||
'$ balena devices supported -vj',
|
||||
'$ balena devices supported --json',
|
||||
];
|
||||
|
||||
public static usage = (
|
||||
@ -58,56 +45,50 @@ export default class DevicesSupportedCmd extends Command {
|
||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||
).trim();
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
discontinued: flags.boolean({
|
||||
description: 'include "discontinued" device types',
|
||||
}),
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
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);
|
||||
const dts = await getBalenaSdk().models.config.getDeviceTypes();
|
||||
let deviceTypes: Array<Partial<SDK.DeviceTypeJson.DeviceType>> = dts.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;
|
||||
const { flags: options } = await this.parse(DevicesSupportedCmd);
|
||||
const pineOptions = {
|
||||
$select: ['slug', 'name'],
|
||||
$expand: {
|
||||
is_of__cpu_architecture: { $select: 'slug' },
|
||||
device_type_alias: {
|
||||
$select: 'is_referenced_by__alias',
|
||||
$orderby: { is_referenced_by__alias: 'asc' },
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!options.discontinued) {
|
||||
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
|
||||
pineOptions,
|
||||
)) as Array<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||
>;
|
||||
interface DT {
|
||||
slug: string;
|
||||
aliases: string[];
|
||||
arch: string;
|
||||
name: string;
|
||||
}
|
||||
const fields = options.verbose
|
||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
||||
: ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(
|
||||
deviceTypes.map((d) => {
|
||||
const picked = _.pick(d, fields);
|
||||
// 'BETA' renamed to 'NEW'
|
||||
picked.state = picked.state === 'BETA' ? 'NEW' : picked.state;
|
||||
return picked;
|
||||
}),
|
||||
fields,
|
||||
);
|
||||
let deviceTypes = dts.map((dt): DT => {
|
||||
const aliases = dt.device_type_alias
|
||||
.map((dta) => dta.is_referenced_by__alias)
|
||||
.filter((alias) => alias !== dt.slug);
|
||||
return {
|
||||
slug: dt.slug,
|
||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
|
||||
name: dt.name || 'N/A',
|
||||
};
|
||||
});
|
||||
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||
} else {
|
||||
|
124
lib/commands/env/add.ts
vendored
124
lib/commands/env/add.ts
vendored
@ -15,16 +15,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
import Command from '../../command';
|
||||
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string; // application name
|
||||
fleet?: string;
|
||||
device?: string; // device UUID
|
||||
help: void;
|
||||
quiet: boolean;
|
||||
@ -38,18 +38,17 @@ interface ArgsDef {
|
||||
|
||||
export default class EnvAddCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Add env or config variable to application(s), device(s) or service(s).
|
||||
Add env or config variable to fleets, devices or services.
|
||||
|
||||
Add an environment or config variable to one or more applications, devices
|
||||
or services, as selected by the respective command-line options. Either the
|
||||
--application or the --device option must be provided, and either may be be
|
||||
Add an environment or config variable to one or more fleets, devices or
|
||||
services, as selected by the respective command-line options. Either the
|
||||
--fleet 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.)
|
||||
(A service corresponds to a Docker image/container in a microservices fleet.)
|
||||
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.
|
||||
the service variable applies to the selected device only. Otherwise, it
|
||||
applies to all devices of the selected 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
|
||||
@ -61,54 +60,55 @@ export default class EnvAddCmd extends Command {
|
||||
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.
|
||||
`;
|
||||
with a reserved prefix. When defining custom fleet variables, please avoid
|
||||
these reserved prefixes.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena env add TERM --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp',
|
||||
'$ balena env add EDITOR vim --application MyApp,MyApp2',
|
||||
'$ balena env add EDITOR vim --application MyApp --service MyService',
|
||||
'$ balena env add EDITOR vim --application MyApp,MyApp2 --service MyService,MyService2',
|
||||
'$ balena env add TERM --fleet MyFleet',
|
||||
'$ balena env add EDITOR vim -f myorg/myfleet',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet --service MyService',
|
||||
'$ balena env add EDITOR vim --fleet MyFleet,MyFleet2 --service MyService,MyService2',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6 --service MyService',
|
||||
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
required: true,
|
||||
description: 'environment or config variable name',
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
}),
|
||||
value: Args.string({
|
||||
required: false,
|
||||
description:
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
// Required for supporting empty string ('') `value` args.
|
||||
public static strict = false;
|
||||
public static usage = 'env add <name> [value]';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
public static flags = {
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
quiet: cf.quiet,
|
||||
service: cf.service,
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvAddCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(EnvAddCmd);
|
||||
const cmd = this;
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
if (!options.fleet && !options.device) {
|
||||
throw new ExpectedError(
|
||||
'Either the --application or the --device option must always be used',
|
||||
'Either the --fleet or the --device option must be specified',
|
||||
);
|
||||
}
|
||||
|
||||
@ -148,16 +148,16 @@ export default class EnvAddCmd extends Command {
|
||||
}
|
||||
|
||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||
if (options.application) {
|
||||
for (const app of options.application.split(',')) {
|
||||
if (options.fleet) {
|
||||
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
|
||||
try {
|
||||
await balena.models.application[varType].set(
|
||||
app,
|
||||
appSlug,
|
||||
params.name,
|
||||
params.value,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, app: ${app}`);
|
||||
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@ -178,26 +178,45 @@ export default class EnvAddCmd extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Stop accepting application names in the next major
|
||||
// and just drop this in favor of doing the .split(',') directly.
|
||||
async function resolveFleetSlugs(
|
||||
balena: BalenaSdk.BalenaSDK,
|
||||
fleetOption: string,
|
||||
) {
|
||||
const fleetSlugs: string[] = [];
|
||||
const { getFleetSlug } = await import('../../utils/sdk');
|
||||
for (const appNameOrSlug of fleetOption.split(',')) {
|
||||
try {
|
||||
fleetSlugs.push(await getFleetSlug(balena, appNameOrSlug));
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, fleet: ${appNameOrSlug}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
return fleetSlugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add service variables for a device or application.
|
||||
* Add service variables for a device or fleet.
|
||||
*/
|
||||
async function setServiceVars(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
params: ArgsDef,
|
||||
options: FlagsDef,
|
||||
) {
|
||||
if (options.application) {
|
||||
for (const app of options.application.split(',')) {
|
||||
if (options.fleet) {
|
||||
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
|
||||
for (const service of options.service!.split(',')) {
|
||||
try {
|
||||
const serviceId = await getServiceIdForApp(sdk, app, service);
|
||||
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
|
||||
await sdk.models.service.var.set(
|
||||
serviceId,
|
||||
params.name,
|
||||
params.value!,
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, application: ${app}`);
|
||||
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
@ -212,7 +231,7 @@ async function setServiceVars(
|
||||
sdk,
|
||||
uuid,
|
||||
['id'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`${err.message}, device: ${uuid}`);
|
||||
@ -221,11 +240,7 @@ async function setServiceVars(
|
||||
}
|
||||
for (const service of options.service!.split(',')) {
|
||||
try {
|
||||
const serviceId = await getServiceIdForApp(
|
||||
sdk,
|
||||
app.app_name,
|
||||
service,
|
||||
);
|
||||
const serviceId = await getServiceIdForApp(sdk, app.slug, service);
|
||||
await sdk.models.device.serviceVar.set(
|
||||
device.id,
|
||||
serviceId,
|
||||
@ -246,11 +261,12 @@ async function setServiceVars(
|
||||
*/
|
||||
async function getServiceIdForApp(
|
||||
sdk: BalenaSdk.BalenaSDK,
|
||||
appName: string,
|
||||
appSlug: string,
|
||||
serviceName: string,
|
||||
): Promise<number> {
|
||||
let serviceId: number | undefined;
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
const services = await sdk.models.service.getAllByApplication(appSlug, {
|
||||
$select: 'id',
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (services.length > 0) {
|
||||
@ -258,7 +274,7 @@ async function getServiceIdForApp(
|
||||
}
|
||||
if (serviceId === undefined) {
|
||||
throw new ExpectedError(
|
||||
`Cannot find service ${serviceName} for application ${appName}`,
|
||||
`Cannot find service ${serviceName} for fleet ${appSlug}`,
|
||||
);
|
||||
}
|
||||
return serviceId;
|
||||
|
42
lib/commands/env/rename.ts
vendored
42
lib/commands/env/rename.ts
vendored
@ -14,7 +14,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as cf from '../../utils/common-flags';
|
||||
@ -22,25 +22,11 @@ import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
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 config or env var for a fleet, device or service.
|
||||
|
||||
Change the value of a configuration or environment variable for an application,
|
||||
Change the value of a configuration or environment variable for a fleet,
|
||||
device or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
@ -54,24 +40,22 @@ export default class EnvRenameCmd extends Command {
|
||||
'$ balena env rename 678678 1 --device --config',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
public static args = {
|
||||
id: Args.integer({
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
parse: async (input) => parseAsInteger(input, 'id'),
|
||||
}),
|
||||
value: Args.string({
|
||||
required: true,
|
||||
description:
|
||||
"variable value; if omitted, use value from this process' environment",
|
||||
},
|
||||
];
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'env rename <id> <value>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
@ -79,9 +63,7 @@ export default class EnvRenameCmd extends Command {
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRenameCmd,
|
||||
);
|
||||
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
|
40
lib/commands/env/rm.ts
vendored
40
lib/commands/env/rm.ts
vendored
@ -15,31 +15,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
|
||||
import * as ec from '../../utils/env-common';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
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 config or env var from a fleet, device or service.
|
||||
|
||||
Remove a configuration or environment variable from an application, device
|
||||
Remove a configuration or environment variable from a fleet, device
|
||||
or service, as selected by command-line options.
|
||||
|
||||
${ec.rmRenameHelp.split('\n').join('\n\t\t')}
|
||||
@ -57,22 +44,21 @@ export default class EnvRmCmd extends Command {
|
||||
'$ balena env rm 789789 --device --service --yes',
|
||||
];
|
||||
|
||||
public static args: Array<IArg<any>> = [
|
||||
{
|
||||
name: 'id',
|
||||
public static args = {
|
||||
id: Args.integer({
|
||||
required: true,
|
||||
description: "variable's numeric database ID",
|
||||
parse: (input) => parseAsInteger(input, 'id'),
|
||||
},
|
||||
];
|
||||
parse: async (input) => parseAsInteger(input, 'id'),
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'env rm <id>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
config: ec.booleanConfig,
|
||||
device: ec.booleanDevice,
|
||||
service: ec.booleanService,
|
||||
yes: flags.boolean({
|
||||
yes: Flags.boolean({
|
||||
char: 'y',
|
||||
description:
|
||||
'do not prompt for confirmation before deleting the variable',
|
||||
@ -81,9 +67,7 @@ export default class EnvRmCmd extends Command {
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
||||
EnvRmCmd,
|
||||
);
|
||||
const { args: params, flags: opt } = await this.parse(EnvRmCmd);
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
@ -91,8 +75,6 @@ export default class EnvRmCmd extends Command {
|
||||
await confirm(
|
||||
opt.yes || false,
|
||||
'Are you sure you want to delete the environment variable?',
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2019 Balena Ltd.
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,62 +14,54 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { flags } from '@oclif/command';
|
||||
import { Flags } from '@oclif/core';
|
||||
import type { Interfaces } from '@oclif/core';
|
||||
import type * as SDK from 'balena-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import Command from '../command';
|
||||
import Command from '../../command';
|
||||
import { ExpectedError } from '../../errors';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
import { ExpectedError } from '../errors';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { isV13 } from '../utils/version';
|
||||
|
||||
interface FlagsDef {
|
||||
application?: string; // application name
|
||||
config: boolean;
|
||||
device?: string; // device UUID
|
||||
json: boolean;
|
||||
help: void;
|
||||
service?: string; // service name
|
||||
verbose: boolean;
|
||||
}
|
||||
type FlagsDef = Interfaces.InferredFlags<typeof EnvsCmd.flags>;
|
||||
|
||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||
appName?: string | null; // application name
|
||||
fleet?: string | null; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface DeviceServiceEnvironmentVariableInfo
|
||||
extends SDK.DeviceServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
fleet?: string; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
interface ServiceEnvironmentVariableInfo
|
||||
extends SDK.ServiceEnvironmentVariable {
|
||||
appName?: string; // application name
|
||||
fleet?: string; // fleet slug
|
||||
deviceUUID?: string; // device UUID
|
||||
serviceName?: string; // service name
|
||||
}
|
||||
|
||||
export default class EnvsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List the environment or config variables of an application, device or service.
|
||||
List the environment or config variables of a fleet, 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.)
|
||||
List the environment or configuration variables of a fleet, device or
|
||||
service, as selected by the respective command-line options. (A service
|
||||
corresponds to a Docker image/container in a microservices fleet.)
|
||||
|
||||
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 results include fleet-wide (multiple devices), device-specific (multiple
|
||||
services on a specific device) and service-specific variables that apply to the
|
||||
selected fleet, device or service. It can be thought of as including inherited
|
||||
variables; for example, a service inherits device-wide variables, and a device
|
||||
inherits fleet-wide variables.
|
||||
|
||||
The printed output may include DEVICE and/or SERVICE columns to distinguish
|
||||
between application-wide, device-specific and service-specific variables.
|
||||
between fleet-wide, device-specific and service-specific variables.
|
||||
An asterisk in these columns indicates that the variable applies to
|
||||
"all devices" or "all services".
|
||||
|
||||
@ -83,18 +75,19 @@ export default class EnvsCmd extends Command {
|
||||
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).
|
||||
`;
|
||||
for the given query. When querying variables for a device, note that the fleet
|
||||
name may be null in JSON output (or 'N/A' in tabular output) if the fleet that
|
||||
the device belonged to is no longer accessible by the current user (for example,
|
||||
in case the current user was removed from the fleet by the fleet's owner).
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ 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 --fleet myorg/myfleet',
|
||||
'$ balena envs --fleet MyFleet --json',
|
||||
'$ balena envs --fleet MyFleet --service MyService',
|
||||
'$ balena envs --fleet MyFleet --config',
|
||||
'$ balena envs --device 7cf02a6',
|
||||
'$ balena envs --device 7cf02a6 --json',
|
||||
'$ balena envs --device 7cf02a6 --config --json',
|
||||
@ -103,78 +96,69 @@ export default class EnvsCmd extends Command {
|
||||
|
||||
public static usage = 'envs';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
all: flags.boolean({
|
||||
default: false,
|
||||
description: stripIndent`
|
||||
No-op since balena CLI v12.0.0.`,
|
||||
hidden: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
application: { exclusive: ['device'], ...cf.application },
|
||||
config: flags.boolean({
|
||||
public static flags = {
|
||||
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||
config: Flags.boolean({
|
||||
default: false,
|
||||
char: 'c',
|
||||
description: 'show configuration variables only',
|
||||
exclusive: ['service'],
|
||||
}),
|
||||
device: { exclusive: ['application'], ...cf.device },
|
||||
device: { ...cf.device, exclusive: ['fleet'] },
|
||||
help: cf.help,
|
||||
json: flags.boolean({
|
||||
default: false,
|
||||
char: 'j',
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
}),
|
||||
verbose: cf.verbose,
|
||||
service: { exclusive: ['config'], ...cf.service },
|
||||
json: cf.json,
|
||||
service: { ...cf.service, exclusive: ['config'] },
|
||||
};
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
||||
const { flags: options } = await this.parse(EnvsCmd);
|
||||
|
||||
const variables: EnvironmentVariableInfo[] = [];
|
||||
|
||||
await Command.checkLoggedIn();
|
||||
|
||||
if (!options.application && !options.device) {
|
||||
throw new ExpectedError('You must specify an application or device');
|
||||
if (!options.fleet && !options.device) {
|
||||
throw new ExpectedError('Missing --fleet or --device option');
|
||||
}
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
let appName = options.application;
|
||||
let fleetSlug: string | undefined = options.fleet
|
||||
? await (
|
||||
await import('../../utils/sdk')
|
||||
).getFleetSlug(balena, options.fleet)
|
||||
: undefined;
|
||||
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||
|
||||
if (options.device) {
|
||||
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
|
||||
const { getDeviceAndMaybeAppFromUUID } = await import(
|
||||
'../../utils/cloud'
|
||||
);
|
||||
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
||||
balena,
|
||||
options.device,
|
||||
['uuid'],
|
||||
['app_name'],
|
||||
['slug'],
|
||||
);
|
||||
fullUUID = device.uuid;
|
||||
if (app) {
|
||||
appName = app.app_name;
|
||||
fleetSlug = app.slug;
|
||||
}
|
||||
}
|
||||
if (appName && options.service) {
|
||||
await validateServiceName(balena, options.service, appName);
|
||||
if (fleetSlug && options.service) {
|
||||
await validateServiceName(balena, options.service, fleetSlug);
|
||||
}
|
||||
variables.push(...(await getAppVars(balena, appName, options)));
|
||||
variables.push(...(await getAppVars(balena, fleetSlug, options)));
|
||||
if (fullUUID) {
|
||||
variables.push(
|
||||
...(await getDeviceVars(balena, fullUUID, appName, options)),
|
||||
...(await getDeviceVars(balena, fullUUID, fleetSlug, options)),
|
||||
);
|
||||
}
|
||||
if (!options.json && variables.length === 0) {
|
||||
const target =
|
||||
(options.service ? `service "${options.service}" of ` : '') +
|
||||
(options.application
|
||||
? `application "${options.application}"`
|
||||
(options.fleet
|
||||
? `fleet "${options.fleet}"`
|
||||
: `device "${options.device}"`);
|
||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||
}
|
||||
@ -186,15 +170,14 @@ export default class EnvsCmd extends Command {
|
||||
varArray: EnvironmentVariableInfo[],
|
||||
options: FlagsDef,
|
||||
) {
|
||||
const fields = ['id', 'name', 'value'];
|
||||
const fields = ['id', 'name', 'value', 'fleet'];
|
||||
|
||||
// Replace undefined app names with 'N/A' or null
|
||||
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
||||
i.appName = i.appName || (options.json ? null : 'N/A');
|
||||
i.fleet ||= options.json ? null : 'N/A';
|
||||
return i;
|
||||
});
|
||||
|
||||
fields.push(options.json ? 'appName' : 'appName => APPLICATION');
|
||||
if (options.device) {
|
||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||
}
|
||||
@ -203,9 +186,9 @@ export default class EnvsCmd extends Command {
|
||||
}
|
||||
|
||||
if (options.json) {
|
||||
this.log(
|
||||
stringifyVarArray<SDK.EnvironmentVariableBase>(varArray, fields),
|
||||
);
|
||||
const { pickAndRename } = await import('../../utils/helpers');
|
||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||
this.log(JSON.stringify(mapped, null, 4));
|
||||
} else {
|
||||
this.log(
|
||||
getVisuals().table.horizontal(
|
||||
@ -220,14 +203,15 @@ export default class EnvsCmd extends Command {
|
||||
async function validateServiceName(
|
||||
sdk: SDK.BalenaSDK,
|
||||
serviceName: string,
|
||||
appName: string,
|
||||
fleetSlug: string,
|
||||
) {
|
||||
const services = await sdk.models.service.getAllByApplication(appName, {
|
||||
const services = await sdk.models.service.getAllByApplication(fleetSlug, {
|
||||
$select: 'id',
|
||||
$filter: { service_name: serviceName },
|
||||
});
|
||||
if (services.length === 0) {
|
||||
throw new ExpectedError(
|
||||
`Service "${serviceName}" not found for application "${appName}"`,
|
||||
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -241,17 +225,18 @@ async function validateServiceName(
|
||||
*/
|
||||
async function getAppVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
appName: string | undefined,
|
||||
fleetSlug: string | undefined,
|
||||
options: FlagsDef,
|
||||
): Promise<EnvironmentVariableInfo[]> {
|
||||
const appVars: EnvironmentVariableInfo[] = [];
|
||||
if (!appName) {
|
||||
if (!fleetSlug) {
|
||||
return appVars;
|
||||
}
|
||||
const vars = await sdk.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(appName);
|
||||
fillInInfoFields(vars, appName);
|
||||
const vars =
|
||||
await sdk.models.application[
|
||||
options.config ? 'configVar' : 'envVar'
|
||||
].getAllByApplication(fleetSlug);
|
||||
fillInInfoFields(vars, fleetSlug);
|
||||
appVars.push(...vars);
|
||||
if (!options.config) {
|
||||
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
|
||||
@ -267,10 +252,10 @@ async function getAppVars(
|
||||
};
|
||||
}
|
||||
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
||||
appName,
|
||||
fleetSlug,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(serviceVars, appName);
|
||||
fillInInfoFields(serviceVars, fleetSlug);
|
||||
appVars.push(...serviceVars);
|
||||
}
|
||||
return appVars;
|
||||
@ -283,16 +268,15 @@ async function getAppVars(
|
||||
async function getDeviceVars(
|
||||
sdk: SDK.BalenaSDK,
|
||||
fullUUID: string,
|
||||
appName: string | undefined,
|
||||
fleetSlug: 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);
|
||||
const deviceConfigVars =
|
||||
await sdk.models.device.configVar.getAllByDevice(fullUUID);
|
||||
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceConfigVars);
|
||||
} else {
|
||||
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
|
||||
@ -313,13 +297,12 @@ async function getDeviceVars(
|
||||
fullUUID,
|
||||
pineOpts,
|
||||
);
|
||||
fillInInfoFields(deviceServiceVars, appName, printedUUID);
|
||||
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceServiceVars);
|
||||
|
||||
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
||||
fullUUID,
|
||||
);
|
||||
fillInInfoFields(deviceEnvVars, appName, printedUUID);
|
||||
const deviceEnvVars =
|
||||
await sdk.models.device.envVar.getAllByDevice(fullUUID);
|
||||
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
|
||||
deviceVars.push(...deviceEnvVars);
|
||||
}
|
||||
return deviceVars;
|
||||
@ -335,7 +318,7 @@ function fillInInfoFields(
|
||||
| EnvironmentVariableInfo[]
|
||||
| DeviceServiceEnvironmentVariableInfo[]
|
||||
| ServiceEnvironmentVariableInfo[],
|
||||
appName?: string,
|
||||
fleetSlug?: string,
|
||||
deviceUUID?: string,
|
||||
) {
|
||||
for (const envVar of varArray) {
|
||||
@ -344,33 +327,13 @@ function fillInInfoFields(
|
||||
envVar.serviceName = (envVar.service as SDK.Service[])[0]?.service_name;
|
||||
} else if ('service_install' in envVar) {
|
||||
// envVar is of type DeviceServiceEnvironmentVariableInfo
|
||||
envVar.serviceName = ((envVar.service_install as SDK.ServiceInstall[])[0]
|
||||
?.installs__service as SDK.Service[])[0]?.service_name;
|
||||
envVar.serviceName = (
|
||||
(envVar.service_install as SDK.ServiceInstall[])[0]
|
||||
?.installs__service as SDK.Service[]
|
||||
)[0]?.service_name;
|
||||
}
|
||||
envVar.appName = appName;
|
||||
envVar.fleet = fleetSlug;
|
||||
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 = varArray.map((o: Dictionary<any>) =>
|
||||
_.transform(
|
||||
o,
|
||||
(result, value, key) => {
|
||||
if (fields.includes(key)) {
|
||||
result[key] = value;
|
||||
}
|
||||
},
|
||||
{} as Dictionary<any>,
|
||||
),
|
||||
);
|
||||
return JSON.stringify(transformed, null, 4);
|
||||
}
|
83
lib/commands/fleet/create.ts
Normal file
83
lib/commands/fleet/create.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flags, Args } from '@oclif/core';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { stripIndent } from '../../utils/lazy';
|
||||
|
||||
export default class FleetCreateCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Create a fleet.
|
||||
|
||||
Create a new balena fleet.
|
||||
|
||||
You can specify the organization the fleet should belong to using
|
||||
the \`--organization\` option. The organization's handle, not its name,
|
||||
should be provided. Organization handles can be listed with the
|
||||
\`balena orgs\` command.
|
||||
|
||||
The fleet's default device type is specified with the \`--type\` option.
|
||||
The \`balena devices supported\` command can be used to list the available
|
||||
device types.
|
||||
|
||||
Interactive dropdowns will be shown for selection if no device type or
|
||||
organization is specified and there are multiple options to choose from.
|
||||
If there is a single option to choose from, it will be chosen automatically.
|
||||
This interactive behavior can be disabled by explicitly specifying a device
|
||||
type and organization.
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet create MyFleet',
|
||||
'$ balena fleet create MyFleet --organization mmyorg',
|
||||
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
name: Args.string({
|
||||
description: 'fleet name',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'fleet create <name>';
|
||||
|
||||
public static flags = {
|
||||
organization: Flags.string({
|
||||
char: 'o',
|
||||
description: 'handle of the organization the fleet should belong to',
|
||||
}),
|
||||
type: Flags.string({
|
||||
char: 't',
|
||||
description:
|
||||
'fleet device type (Check available types with `balena devices supported`)',
|
||||
}),
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(FleetCreateCmd);
|
||||
|
||||
await (
|
||||
await import('../../utils/application-create')
|
||||
).applicationCreateBase('fleet', options, params);
|
||||
}
|
||||
}
|
93
lib/commands/fleet/index.ts
Normal file
93
lib/commands/fleet/index.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class FleetCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Display information about a single fleet.
|
||||
|
||||
Display detailed information about a single fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet MyFleet',
|
||||
'$ balena fleet myorg/myfleet',
|
||||
'$ balena fleet myorg/myfleet --view',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
};
|
||||
|
||||
public static usage = 'fleet <fleet>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
view: Flags.boolean({
|
||||
default: false,
|
||||
description: 'open fleet dashboard page',
|
||||
}),
|
||||
...cf.dataOutputFlags,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = await this.parse(FleetCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
should_be_running__release: { $select: 'commit' },
|
||||
},
|
||||
});
|
||||
|
||||
if (options.view) {
|
||||
const open = await import('open');
|
||||
const dashboardUrl = balena.models.application.getDashboardUrl(
|
||||
application.id,
|
||||
);
|
||||
await open(dashboardUrl, { wait: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const outputApplication = {
|
||||
...application,
|
||||
device_type: application.is_for__device_type[0].slug,
|
||||
commit: application.should_be_running__release[0]?.commit,
|
||||
};
|
||||
|
||||
await this.outputData(
|
||||
outputApplication,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
88
lib/commands/fleet/pin.ts
Normal file
88
lib/commands/fleet/pin.ts
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getExpandedProp } from '../../utils/pine';
|
||||
|
||||
export default class FleetPinCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Pin a fleet to a release.
|
||||
|
||||
Pin a fleet to a release.
|
||||
|
||||
Note, if the commit is omitted, the currently pinned release will be printed, with instructions for how to see a list of releases
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet pin myfleet',
|
||||
'$ balena fleet pin myorg/myfleet 91165e5',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
slug: Args.string({
|
||||
description: 'the slug of the fleet to pin to a release',
|
||||
required: true,
|
||||
}),
|
||||
releaseToPinTo: Args.string({
|
||||
description: 'the commit of the release for the fleet to get pinned to',
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'fleet pin <slug> [releaseToPinTo]';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(FleetPinCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const fleet = await balena.models.application.get(params.slug, {
|
||||
$expand: {
|
||||
should_be_running__release: {
|
||||
$select: 'commit',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const pinnedRelease = getExpandedProp(
|
||||
fleet.should_be_running__release,
|
||||
'commit',
|
||||
);
|
||||
|
||||
const releaseToPinTo = params.releaseToPinTo;
|
||||
const slug = params.slug;
|
||||
|
||||
if (!releaseToPinTo) {
|
||||
console.log(
|
||||
`${
|
||||
pinnedRelease
|
||||
? `This fleet is currently pinned to ${pinnedRelease}.`
|
||||
: 'This fleet is not currently pinned to any release.'
|
||||
} \n\nTo see a list of all releases this fleet can be pinned to, run \`balena releases ${slug}\`.`,
|
||||
);
|
||||
} else {
|
||||
await balena.models.application.pinToRelease(slug, releaseToPinTo);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,61 +15,54 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
export default class FleetPurgeCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Purge data from an application.
|
||||
Purge data from a fleet.
|
||||
|
||||
Purge data from all devices belonging to an application.
|
||||
This will clear the application's /data directory.
|
||||
`;
|
||||
public static examples = ['$ balena app purge MyApp'];
|
||||
Purge data from all devices belonging to a fleet.
|
||||
This will clear the fleet's '/data' directory.
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet purge MyFleet',
|
||||
'$ balena fleet purge myorg/myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'app purge <name>';
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
};
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static usage = 'fleet purge <fleet>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
const { args: params } = await this.parse(FleetPurgeCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// balena.models.application.purge only accepts a numeric id
|
||||
// so we must first fetch the app to get it's id, if we have been given a name
|
||||
let nameOrId = tryAsInteger(params.name);
|
||||
|
||||
if (typeof nameOrId === 'string') {
|
||||
const app = await balena.models.application.get(nameOrId);
|
||||
nameOrId = app.id;
|
||||
}
|
||||
// so we must first fetch the app to get it's id,
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$select: 'id',
|
||||
});
|
||||
|
||||
try {
|
||||
await balena.models.application.purge(nameOrId);
|
||||
await balena.models.application.purge(application.id);
|
||||
} catch (e) {
|
||||
if (e.message.toLowerCase().includes('no online device(s) found')) {
|
||||
// application.purge throws an error if no devices are online
|
139
lib/commands/fleet/rename.ts
Normal file
139
lib/commands/fleet/rename.ts
Normal file
@ -0,0 +1,139 @@
|
||||
/**
|
||||
* @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 { Args } from '@oclif/core';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class FleetRenameCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Rename a fleet.
|
||||
|
||||
Rename a fleet.
|
||||
|
||||
Note, if the \`newName\` parameter is omitted, it will be
|
||||
prompted for interactively.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet rename OldName',
|
||||
'$ balena fleet rename OldName NewName',
|
||||
'$ balena fleet rename myorg/oldname NewName',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
newName: Args.string({
|
||||
description: 'the new name for the fleet',
|
||||
}),
|
||||
};
|
||||
|
||||
public static usage = 'fleet rename <fleet> [newName]';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(FleetRenameCmd);
|
||||
|
||||
const { validateApplicationName } = await import('../../utils/validation');
|
||||
const { ExpectedError } = await import('../../errors');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate target application (if params.params is a number, it could either be an ID or a numerical name)
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
application_type: {
|
||||
$select: 'slug',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Check app exists
|
||||
if (!application) {
|
||||
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
|
||||
}
|
||||
|
||||
// Check app supports renaming
|
||||
const appType = application.application_type[0];
|
||||
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||
throw new ExpectedError(
|
||||
`Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Ascertain new name
|
||||
const newName =
|
||||
params.newName ||
|
||||
(await getCliForm().ask({
|
||||
message: 'Please enter the new name for this fleet:',
|
||||
type: 'input',
|
||||
validate: validateApplicationName,
|
||||
})) ||
|
||||
'';
|
||||
|
||||
// Check they haven't used slug in new name
|
||||
if (newName.includes('/')) {
|
||||
throw new ExpectedError(
|
||||
`New fleet name cannot include '/', please check that you are not specifying fleet slug.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Rename
|
||||
try {
|
||||
await balena.models.application.rename(application.id, newName);
|
||||
} catch (e) {
|
||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
||||
if ((e.message || '').toLowerCase().includes('unique')) {
|
||||
throw new ExpectedError(`Error: fleet ${newName} already exists.`);
|
||||
}
|
||||
// BalenaRequestError: Request error: App name may only contain [a-zA-Z0-9_-].
|
||||
if ((e.message || '').toLowerCase().includes('name may only contain')) {
|
||||
throw new ExpectedError(
|
||||
`Error: new fleet name may only include characters [a-zA-Z0-9_-].`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Get application again, to be sure of results
|
||||
const renamedApplication = await getApplication(balena, application.id, {
|
||||
$select: ['app_name', 'slug'],
|
||||
});
|
||||
|
||||
// Output result
|
||||
console.log(`Fleet renamed`);
|
||||
console.log('From:');
|
||||
console.log(`\tname: ${application.app_name}`);
|
||||
console.log(`\tslug: ${application.slug}`);
|
||||
console.log('To:');
|
||||
console.log(`\tname: ${renamedApplication.app_name}`);
|
||||
console.log(`\tslug: ${renamedApplication.slug}`);
|
||||
}
|
||||
}
|
64
lib/commands/fleet/restart.ts
Normal file
64
lib/commands/fleet/restart.ts
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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 Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class FleetRestartCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart a fleet.
|
||||
|
||||
Restart all devices belonging to a fleet.
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena fleet restart MyFleet',
|
||||
'$ balena fleet restart myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
};
|
||||
|
||||
public static usage = 'fleet restart <fleet>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = await this.parse(FleetRestartCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Disambiguate application
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$select: 'slug',
|
||||
});
|
||||
|
||||
await balena.models.application.restart(application.slug);
|
||||
}
|
||||
}
|
@ -15,45 +15,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
interface FlagsDef {
|
||||
yes: boolean;
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRmCmd extends Command {
|
||||
export default class FleetRmCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Remove an application.
|
||||
Remove a fleet.
|
||||
|
||||
Permanently remove a balena application.
|
||||
Permanently remove a fleet.
|
||||
|
||||
The --yes option may be used to avoid interactive confirmation.
|
||||
`;
|
||||
|
||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||
`;
|
||||
|
||||
public static examples = [
|
||||
'$ balena app rm MyApp',
|
||||
'$ balena app rm MyApp --yes',
|
||||
'$ balena fleet rm MyFleet',
|
||||
'$ balena fleet rm MyFleet --yes',
|
||||
'$ balena fleet rm myorg/myfleet',
|
||||
];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
public static args = {
|
||||
fleet: ca.fleetRequired,
|
||||
};
|
||||
|
||||
public static usage = 'app rm <name>';
|
||||
public static usage = 'fleet rm <fleet>';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static flags = {
|
||||
yes: cf.yes,
|
||||
help: cf.help,
|
||||
};
|
||||
@ -61,19 +52,24 @@ export default class AppRmCmd extends Command {
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||
AppRmCmd,
|
||||
);
|
||||
const { args: params, flags: options } = await this.parse(FleetRmCmd);
|
||||
|
||||
const patterns = await import('../../utils/patterns');
|
||||
const { confirm } = await import('../../utils/patterns');
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
// Confirm
|
||||
await patterns.confirm(
|
||||
await confirm(
|
||||
options.yes ?? false,
|
||||
`Are you sure you want to delete application ${params.name}?`,
|
||||
`Are you sure you want to delete fleet ${params.fleet}?`,
|
||||
);
|
||||
|
||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||
const application = await getApplication(balena, params.fleet, {
|
||||
$select: 'slug',
|
||||
});
|
||||
|
||||
// Remove
|
||||
await getBalenaSdk().models.application.remove(tryAsInteger(params.name));
|
||||
await balena.models.application.remove(application.slug);
|
||||
}
|
||||
}
|
@ -15,47 +15,42 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import { Args } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { tryAsInteger } from '../../utils/validation';
|
||||
|
||||
interface FlagsDef {
|
||||
help: void;
|
||||
}
|
||||
|
||||
interface ArgsDef {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default class AppRestartCmd extends Command {
|
||||
export default class FleetTrackLatestCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
Restart an application.
|
||||
Make this fleet track the latest release.
|
||||
|
||||
Restart all devices belonging to an application.
|
||||
`;
|
||||
public static examples = ['$ balena app restart MyApp'];
|
||||
|
||||
public static args = [
|
||||
{
|
||||
name: 'name',
|
||||
description: 'application name or numeric ID',
|
||||
required: true,
|
||||
},
|
||||
Make this fleet track the latest release.
|
||||
`;
|
||||
public static examples = [
|
||||
'$ balena fleet track-latest myorg/myfleet',
|
||||
'$ balena fleet track-latest myfleet',
|
||||
];
|
||||
|
||||
public static usage = 'app restart <name>';
|
||||
public static args = {
|
||||
slug: Args.string({
|
||||
description: 'the slug of the fleet to make track the latest release',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
public static usage = 'fleet track-latest <slug>';
|
||||
|
||||
public static flags = {
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
|
||||
public async run() {
|
||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
||||
const { args: params } = await this.parse(FleetTrackLatestCmd);
|
||||
|
||||
await getBalenaSdk().models.application.restart(tryAsInteger(params.name));
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
await balena.models.application.trackLatestRelease(params.slug);
|
||||
}
|
||||
}
|
93
lib/commands/fleets/index.ts
Normal file
93
lib/commands/fleets/index.ts
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2016-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
|
||||
device_count: number;
|
||||
online_devices: number;
|
||||
device_type?: string;
|
||||
}
|
||||
|
||||
export default class FleetsCmd extends Command {
|
||||
public static description = stripIndent`
|
||||
List all fleets.
|
||||
|
||||
List all your balena fleets.
|
||||
|
||||
For detailed information on a particular fleet, use
|
||||
\`balena fleet <fleet>\`
|
||||
`;
|
||||
|
||||
public static examples = ['$ balena fleets'];
|
||||
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags = {
|
||||
...cf.dataSetOutputFlags,
|
||||
help: cf.help,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run() {
|
||||
const { flags: options } = await this.parse(FleetsCmd);
|
||||
|
||||
const balena = getBalenaSdk();
|
||||
|
||||
const pineOptions = {
|
||||
$select: ['id', 'app_name', 'slug'],
|
||||
$expand: {
|
||||
is_for__device_type: { $select: 'slug' },
|
||||
owns__device: { $select: 'is_online' },
|
||||
},
|
||||
} satisfies BalenaSdk.PineOptions<BalenaSdk.Application>;
|
||||
// Get applications
|
||||
const applications =
|
||||
(await balena.models.application.getAllDirectlyAccessible(
|
||||
pineOptions,
|
||||
)) as Array<
|
||||
BalenaSdk.PineTypedResult<BalenaSdk.Application, typeof pineOptions>
|
||||
> as ExtendedApplication[];
|
||||
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices =
|
||||
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user