mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-25 02:47:55 +00:00
Compare commits
641 Commits
v12.49.0
...
npm-global
Author | SHA1 | Date | |
---|---|---|---|
1f1af6d657 | |||
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 | |||
112a7b8194 |
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
|
*.* -eol
|
||||||
|
|
||||||
*.sh text eol=lf
|
*.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
|
# 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
|
# crlf for the eol conversion test files
|
||||||
tests/test-data/projects/docker-compose/basic/service2/file2-crlf.sh eol=crlf
|
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
|
tests/test-data/projects/no-docker-compose/basic/src/windows-crlf.sh eol=crlf
|
||||||
|
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: '18.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: '18.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
|
@ -3,5 +3,8 @@ module.exports = {
|
|||||||
require: 'ts-node/register/transpile-only',
|
require: 'ts-node/register/transpile-only',
|
||||||
file: './tests/config-tests',
|
file: './tests/config-tests',
|
||||||
timeout: 12000,
|
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',
|
spec: 'tests/**/*.spec.ts',
|
||||||
};
|
};
|
||||||
|
17
.resinci.yml
17
.resinci.yml
@ -1,17 +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"
|
|
File diff suppressed because it is too large
Load Diff
3515
CHANGELOG.md
3515
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...)
|
## 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
|
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
|
[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/).
|
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
|
* [Selected
|
||||||
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
sections](https://github.com/balena-io/balena-cli/blob/v12.23.0/automation/capitanodoc/capitanodoc.ts#L199-L204)
|
||||||
@ -120,11 +120,58 @@ The content sources for the auto generation of `doc/cli.markdown` are:
|
|||||||
* `lib/commands/env/add.ts`
|
* `lib/commands/env/add.ts`
|
||||||
|
|
||||||
The README file is manually edited, but subsections are automatically extracted for inclusion in
|
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).
|
[`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.
|
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
|
## Windows
|
||||||
|
|
||||||
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
Besides the regular npm installation dependencies, the `npm run build:installer` script
|
||||||
@ -267,4 +314,3 @@ gotchas to bear in mind:
|
|||||||
replace: `spec: 'tests/**/*.spec.ts',`
|
replace: `spec: 'tests/**/*.spec.ts',`
|
||||||
|
|
||||||
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
with: `spec: ['tests/auth/*.spec.ts', 'tests/**/deploy.spec.ts'],`
|
||||||
|
|
||||||
|
@ -78,8 +78,8 @@ If you are a Node.js developer, you may wish to install the balena CLI via [npm]
|
|||||||
The npm installation involves building native (platform-specific) binary modules, which require
|
The npm installation involves building native (platform-specific) binary modules, which require
|
||||||
some development tools to be installed first, as follows.
|
some development tools to be installed first, as follows.
|
||||||
|
|
||||||
> **The balena CLI currently requires Node.js version 10 (min 10.20.0) or 12.**
|
> **The balena CLI currently requires Node.js version 18.**
|
||||||
> **Versions 13 and later are not yet fully supported.**
|
> **Versions 19 and later are not yet fully supported.**
|
||||||
|
|
||||||
### Install development tools
|
### Install development tools
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ some development tools to be installed first, as follows.
|
|||||||
$ sudo apt-get update && sudo apt-get -y install curl python3 git make g++
|
$ 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
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 12
|
$ nvm install 18
|
||||||
```
|
```
|
||||||
|
|
||||||
The `curl` command line above uses
|
The `curl` command line above uses
|
||||||
@ -106,15 +106,15 @@ recommended.
|
|||||||
```sh
|
```sh
|
||||||
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
|
||||||
$ . ~/.bashrc
|
$ . ~/.bashrc
|
||||||
$ nvm install 12
|
$ nvm install 18
|
||||||
```
|
```
|
||||||
|
|
||||||
#### **Windows** (not WSL)
|
#### **Windows** (not WSL)
|
||||||
|
|
||||||
Install:
|
Install:
|
||||||
|
|
||||||
* Node.js v12 from the [Nodejs.org releases page](https://nodejs.org/en/download/releases/).
|
|
||||||
* If you'd like the ability to switch between Node.js versions, install
|
* If you'd like the ability to switch between Node.js versions, install
|
||||||
|
- Node.js v18 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)
|
[nvm-windows](https://github.com/coreybutler/nvm-windows#node-version-manager-nvm-for-windows)
|
||||||
instead.
|
instead.
|
||||||
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
* The [MSYS2 shell](https://www.msys2.org/), which provides `git`, `make`, `g++` and more:
|
||||||
@ -141,7 +141,8 @@ $ npm install balena-cli --global --production --unsafe-perm
|
|||||||
```
|
```
|
||||||
|
|
||||||
`--unsafe-perm` is needed when `npm install` is executed as the `root` user (e.g. in a Docker
|
`--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.
|
container) in order to allow npm scripts like `postinstall` to be executed. The `--global` flag is needed so
|
||||||
|
the install uses the `npm-shrinkwrap.json` lockfile when [downloading dependencies](https://docs.npmjs.com/cli/v9/configuring-npm/npm-shrinkwrap-json#description).
|
||||||
|
|
||||||
## Additional Dependencies
|
## Additional Dependencies
|
||||||
|
|
||||||
|
@ -144,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 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)
|
* 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
|
For CLI bug reports or feature requests, check the
|
||||||
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
[CLI GitHub issues](https://github.com/balena-io/balena-cli/issues/).
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
* limitations under the License.
|
* 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 archiver from 'archiver';
|
||||||
import * as Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
@ -25,11 +25,11 @@ import * as filehound from 'filehound';
|
|||||||
import { Stats } from 'fs';
|
import { Stats } from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as klaw from 'klaw';
|
import * as klaw from 'klaw';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as rimraf from 'rimraf';
|
import * as rimraf from 'rimraf';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import { notarize } from '@electron/notarize';
|
||||||
|
|
||||||
import { stripIndent } from '../build/utils/lazy';
|
import { stripIndent } from '../build/utils/lazy';
|
||||||
import {
|
import {
|
||||||
@ -45,8 +45,6 @@ const execFileAsync = promisify(execFile);
|
|||||||
export const packageJSON = loadPackageJson();
|
export const packageJSON = loadPackageJson();
|
||||||
export const version = 'v' + packageJSON.version;
|
export const version = 'v' + packageJSON.version;
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
const MSYS2_BASH =
|
|
||||||
process.env.MSYSSHELLPATH || 'C:\\msys64\\usr\\bin\\bash.exe';
|
|
||||||
|
|
||||||
function dPath(...paths: string[]) {
|
function dPath(...paths: string[]) {
|
||||||
return path.join(ROOT, 'dist', ...paths);
|
return path.join(ROOT, 'dist', ...paths);
|
||||||
@ -64,7 +62,7 @@ const standaloneZips: PathByPlatform = {
|
|||||||
|
|
||||||
const oclifInstallers: PathByPlatform = {
|
const oclifInstallers: PathByPlatform = {
|
||||||
darwin: dPath('macos', `balena-${version}.pkg`),
|
darwin: dPath('macos', `balena-${version}.pkg`),
|
||||||
win32: dPath('win', `balena-${version}-${arch}.exe`),
|
win32: dPath('win32', `balena-${version}-${arch}.exe`),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renamedOclifInstallers: PathByPlatform = {
|
const renamedOclifInstallers: PathByPlatform = {
|
||||||
@ -89,13 +87,14 @@ async function diffPkgOutput(pkgOut: string) {
|
|||||||
'tests',
|
'tests',
|
||||||
'test-data',
|
'test-data',
|
||||||
'pkg',
|
'pkg',
|
||||||
`expected-warnings-${process.platform}.txt`,
|
`expected-warnings-${process.platform}-${arch}.txt`,
|
||||||
);
|
);
|
||||||
const absSavedPath = path.join(ROOT, relSavedPath);
|
const absSavedPath = path.join(ROOT, relSavedPath);
|
||||||
const ignoreStartsWith = [
|
const ignoreStartsWith = [
|
||||||
'> pkg@',
|
'> pkg@',
|
||||||
'> Fetching base Node.js binaries',
|
'> Fetching base Node.js binaries',
|
||||||
' fetched-',
|
' fetched-',
|
||||||
|
'prebuild-install WARN install No prebuilt binaries found',
|
||||||
];
|
];
|
||||||
const modulesRE =
|
const modulesRE =
|
||||||
process.platform === 'win32'
|
process.platform === 'win32'
|
||||||
@ -181,9 +180,18 @@ async function execPkg(...args: any[]) {
|
|||||||
* to be directly executed from inside another binary executable.)
|
* to be directly executed from inside another binary executable.)
|
||||||
*/
|
*/
|
||||||
async function buildPkg() {
|
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 = [
|
const args = [
|
||||||
'--target',
|
'--targets',
|
||||||
'host',
|
targets,
|
||||||
'--output',
|
'--output',
|
||||||
'build-bin/balena',
|
'build-bin/balena',
|
||||||
'package.json',
|
'package.json',
|
||||||
@ -198,7 +206,6 @@ async function buildPkg() {
|
|||||||
const paths: Array<[string, string[], string[]]> = [
|
const paths: Array<[string, string[], string[]]> = [
|
||||||
// [platform, [source path], [destination path]]
|
// [platform, [source path], [destination path]]
|
||||||
['*', ['open', 'xdg-open'], ['xdg-open']],
|
['*', ['open', 'xdg-open'], ['xdg-open']],
|
||||||
['*', ['opn', 'xdg-open'], ['xdg-open-402']],
|
|
||||||
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
['darwin', ['denymount', 'bin', 'denymount'], ['denymount']],
|
||||||
];
|
];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
@ -310,7 +317,7 @@ async function zipPkg() {
|
|||||||
archive.on('warning', console.warn);
|
archive.on('warning', console.warn);
|
||||||
|
|
||||||
archive.pipe(outputStream);
|
archive.pipe(outputStream);
|
||||||
archive.finalize();
|
archive.finalize().catch(reject);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,6 +329,10 @@ async function signFilesForNotarization() {
|
|||||||
if (!item.stats.isFile()) {
|
if (!item.stats.isFile()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (path.basename(item.path).endsWith('.node.bak')) {
|
||||||
|
console.log('Removing pkg .node.bak file', item.path);
|
||||||
|
fs.unlinkSync(item.path);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
path.basename(item.path).endsWith('.zip') &&
|
path.basename(item.path).endsWith('.zip') &&
|
||||||
path.dirname(item.path).includes('test')
|
path.dirname(item.path).includes('test')
|
||||||
@ -420,20 +431,28 @@ async function renameInstallerFiles() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* If the CSC_LINK and CSC_KEY_PASSWORD env vars are set, digitally sign the
|
* 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
|
* executable installer using Microsoft SignTool.exe (Sign Tool)
|
||||||
* script (which must be in the PATH) using a MSYS2 bash shell.
|
* https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe
|
||||||
*/
|
*/
|
||||||
async function signWindowsInstaller() {
|
async function signWindowsInstaller() {
|
||||||
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
if (process.env.CSC_LINK && process.env.CSC_KEY_PASSWORD) {
|
||||||
const exeName = renamedOclifInstallers[process.platform];
|
const exeName = renamedOclifInstallers[process.platform];
|
||||||
console.log(`Signing installer "${exeName}"`);
|
console.log(`Signing installer "${exeName}"`);
|
||||||
await execFileAsync(MSYS2_BASH, [
|
// trust ...
|
||||||
'sign-exe.sh',
|
await execFileAsync('signtool.exe', [
|
||||||
|
'sign',
|
||||||
|
'-t',
|
||||||
|
process.env.TIMESTAMP_SERVER || 'http://timestamp.comodoca.com',
|
||||||
'-f',
|
'-f',
|
||||||
exeName,
|
process.env.CSC_LINK,
|
||||||
|
'-p',
|
||||||
|
process.env.CSC_KEY_PASSWORD,
|
||||||
'-d',
|
'-d',
|
||||||
`balena-cli ${version}`,
|
`balena-cli ${version}`,
|
||||||
|
exeName,
|
||||||
]);
|
]);
|
||||||
|
// ... but verify
|
||||||
|
await execFileAsync('signtool.exe', ['verify', '-pa', '-v', exeName]);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
'Skipping installer signing step because CSC_* env vars are not set',
|
'Skipping installer signing step because CSC_* env vars are not set',
|
||||||
@ -445,18 +464,24 @@ async function signWindowsInstaller() {
|
|||||||
* Wait for Apple Installer Notarization to continue
|
* Wait for Apple Installer Notarization to continue
|
||||||
*/
|
*/
|
||||||
async function notarizeMacInstaller(): Promise<void> {
|
async function notarizeMacInstaller(): Promise<void> {
|
||||||
const appleId = 'accounts+apple@balena.io';
|
const teamId = process.env.XCODE_APP_LOADER_TEAM_ID || '66H43P8FRG';
|
||||||
const { notarize } = await import('electron-notarize');
|
const appleId =
|
||||||
await notarize({
|
process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io';
|
||||||
appBundleId: 'io.balena.etcher',
|
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD;
|
||||||
appPath: renamedOclifInstallers.darwin,
|
|
||||||
appleId,
|
if (appleIdPassword && teamId) {
|
||||||
appleIdPassword: '@keychain:CLI_PASSWORD',
|
await notarize({
|
||||||
});
|
tool: 'notarytool',
|
||||||
|
teamId,
|
||||||
|
appPath: renamedOclifInstallers.darwin,
|
||||||
|
appleId,
|
||||||
|
appleIdPassword,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the `oclif-dev pack:win` or `pack:macos` command (depending on the value
|
* 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
|
* of process.platform) to generate the native installers (which end up under
|
||||||
* the 'dist' folder). There are some harcoded options such as selecting only
|
* the 'dist' folder). There are some harcoded options such as selecting only
|
||||||
* 64-bit binaries under Windows.
|
* 64-bit binaries under Windows.
|
||||||
@ -466,9 +491,10 @@ export async function buildOclifInstaller() {
|
|||||||
let packOpts = ['-r', ROOT];
|
let packOpts = ['-r', ROOT];
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
packOS = 'macos';
|
packOS = 'macos';
|
||||||
|
packOpts = packOpts.concat('--targets', 'darwin-x64');
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
packOS = 'win';
|
packOS = 'win';
|
||||||
packOpts = packOpts.concat('-t', 'win32-x64');
|
packOpts = packOpts.concat('--targets', 'win32-x64');
|
||||||
}
|
}
|
||||||
if (packOS) {
|
if (packOS) {
|
||||||
console.log(`Building oclif installer for CLI ${version}`);
|
console.log(`Building oclif installer for CLI ${version}`);
|
||||||
@ -486,10 +512,11 @@ export async function buildOclifInstaller() {
|
|||||||
await signFilesForNotarization();
|
await signFilesForNotarization();
|
||||||
}
|
}
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);
|
console.log(`oclif ${packCmd} ${packOpts.join(' ')}`);
|
||||||
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
console.log(`cwd="${process.cwd()}" ROOT="${ROOT}"`);
|
||||||
console.log('=======================================================');
|
console.log('=======================================================');
|
||||||
await oclifRun([packCmd].concat(...packOpts));
|
const oclifPath = path.join(ROOT, 'node_modules', 'oclif');
|
||||||
|
await oclifRun([packCmd].concat(...packOpts), oclifPath);
|
||||||
await renameInstallerFiles();
|
await renameInstallerFiles();
|
||||||
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
// The Windows installer is explicitly signed here (oclif doesn't do it).
|
||||||
// The macOS installer is automatically signed by oclif (which runs the
|
// The macOS installer is automatically signed by oclif (which runs the
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { MarkdownFileParser } from './utils';
|
import { MarkdownFileParser } from './utils';
|
||||||
|
import { GlobSync } from 'glob';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the skeleton of CLI documentation/reference web page at:
|
* This is the skeleton of CLI documentation/reference web page at:
|
||||||
@ -24,172 +25,114 @@ import { MarkdownFileParser } from './utils';
|
|||||||
*
|
*
|
||||||
* The `getCapitanoDoc` function in this module parses README.md and adds
|
* The `getCapitanoDoc` function in this module parses README.md and adds
|
||||||
* some content to this object.
|
* 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',
|
interface Category {
|
||||||
introduction: '',
|
title: string;
|
||||||
categories: [
|
files: string[];
|
||||||
{
|
}
|
||||||
title: 'API keys',
|
|
||||||
files: ['build/commands/api-key/generate.js'],
|
interface Documentation {
|
||||||
},
|
title: string;
|
||||||
{
|
introduction: string;
|
||||||
title: 'Fleet',
|
categories: Category[];
|
||||||
files: [
|
}
|
||||||
'build/commands/apps.js',
|
|
||||||
'build/commands/fleets.js',
|
// Mapping folders names to custom headings in the docs
|
||||||
'build/commands/app/index.js',
|
const commandHeadings: { [key: string]: string } = {
|
||||||
'build/commands/fleet/index.js',
|
'api-key': 'API Key',
|
||||||
'build/commands/app/create.js',
|
'api-keys': 'API Keys',
|
||||||
'build/commands/fleet/create.js',
|
login: 'Authentication',
|
||||||
'build/commands/app/purge.js',
|
whoami: 'Authentication',
|
||||||
'build/commands/fleet/purge.js',
|
logout: 'Authentication',
|
||||||
'build/commands/app/rename.js',
|
env: 'Environment Variable',
|
||||||
'build/commands/fleet/rename.js',
|
envs: 'Environment Variables',
|
||||||
'build/commands/app/restart.js',
|
help: 'Help and Version',
|
||||||
'build/commands/fleet/restart.js',
|
key: 'SSH Key',
|
||||||
'build/commands/app/rm.js',
|
keys: 'SSH Keys',
|
||||||
'build/commands/fleet/rm.js',
|
orgs: 'Organizations',
|
||||||
],
|
os: 'OS',
|
||||||
},
|
util: 'Utilities',
|
||||||
{
|
ssh: 'Network',
|
||||||
title: 'Authentication',
|
scan: 'Network',
|
||||||
files: [
|
tunnel: 'Network',
|
||||||
'build/commands/login.js',
|
build: 'Deploy',
|
||||||
'build/commands/logout.js',
|
join: 'Platform',
|
||||||
'build/commands/whoami.js',
|
leave: 'Platform',
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Device',
|
|
||||||
files: [
|
|
||||||
'build/commands/devices/index.js',
|
|
||||||
'build/commands/devices/supported.js',
|
|
||||||
'build/commands/device/index.js',
|
|
||||||
'build/commands/device/deactivate.js',
|
|
||||||
'build/commands/device/identify.js',
|
|
||||||
'build/commands/device/init.js',
|
|
||||||
'build/commands/device/local-mode.js',
|
|
||||||
'build/commands/device/move.js',
|
|
||||||
'build/commands/device/os-update.js',
|
|
||||||
'build/commands/device/public-url.js',
|
|
||||||
'build/commands/device/purge.js',
|
|
||||||
'build/commands/device/reboot.js',
|
|
||||||
'build/commands/device/register.js',
|
|
||||||
'build/commands/device/rename.js',
|
|
||||||
'build/commands/device/restart.js',
|
|
||||||
'build/commands/device/rm.js',
|
|
||||||
'build/commands/device/shutdown.js',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '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'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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
|
* Modify and return the `capitanoDoc` object above in order to generate the
|
||||||
* CLI documentation/reference web page at:
|
* CLI documentation at docs/balena-cli.md
|
||||||
* https://www.balena.io/docs/reference/cli/
|
|
||||||
*
|
*
|
||||||
* This function parses the README.md file to extract relevant sections
|
* This function parses the README.md file to extract relevant sections
|
||||||
* for the documentation web page.
|
* 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
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Command as OclifCommandClass } from '@oclif/command';
|
import { Command as OclifCommandClass } from '@oclif/core';
|
||||||
|
|
||||||
type OclifCommand = typeof OclifCommandClass;
|
type OclifCommand = typeof OclifCommandClass;
|
||||||
|
|
||||||
|
@ -62,12 +62,11 @@ class FakeHelpCommand {
|
|||||||
'$ balena help os download',
|
'$ balena help os download',
|
||||||
];
|
];
|
||||||
|
|
||||||
args = [
|
args = {
|
||||||
{
|
command: {
|
||||||
name: 'command',
|
|
||||||
description: 'command to show help for',
|
description: 'command to show help for',
|
||||||
},
|
},
|
||||||
];
|
};
|
||||||
|
|
||||||
usage = 'help [command]';
|
usage = 'help [command]';
|
||||||
|
|
||||||
@ -105,4 +104,5 @@ async function printMarkdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
printMarkdown();
|
printMarkdown();
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flagUsages } from '@oclif/parser';
|
import { Parser } from '@oclif/core';
|
||||||
import * as ent from 'ent';
|
import * as ent from 'ent';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
@ -37,8 +37,8 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
|||||||
|
|
||||||
if (!_.isEmpty(command.args)) {
|
if (!_.isEmpty(command.args)) {
|
||||||
result.push('### Arguments');
|
result.push('### Arguments');
|
||||||
for (const arg of command.args!) {
|
for (const [name, arg] of Object.entries(command.args!)) {
|
||||||
result.push(`#### ${arg.name.toUpperCase()}`, arg.description || '');
|
result.push(`#### ${name.toUpperCase()}`, arg.description || '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
flag.name = name;
|
flag.name = name;
|
||||||
const flagUsage = flagUsages([flag])
|
const flagUsage = Parser.flagUsages([flag])
|
||||||
.map(([usage, _description]) => usage)
|
.map(([usage, _description]) => usage)
|
||||||
.join()
|
.join()
|
||||||
.trim();
|
.trim();
|
||||||
|
@ -15,24 +15,25 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const stripIndent = require('common-tags/lib/stripIndent');
|
// eslint-disable-next-line no-restricted-imports
|
||||||
const _ = require('lodash');
|
import { stripIndent } from 'common-tags';
|
||||||
const { promises: fs } = require('fs');
|
import * as _ from 'lodash';
|
||||||
const path = require('path');
|
import { promises as fs } from 'fs';
|
||||||
const simplegit = require('simple-git/promise');
|
import * as path from 'path';
|
||||||
|
import { simpleGit } from 'simple-git';
|
||||||
|
|
||||||
const ROOT = path.normalize(path.join(__dirname, '..'));
|
const ROOT = path.normalize(path.join(__dirname, '..'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare the timestamp of cli.markdown with the timestamp of staged files,
|
* Compare the timestamp of balena-cli.md with the timestamp of staged files,
|
||||||
* issuing an error if cli.markdown is older.
|
* issuing an error if balena-cli.md is older.
|
||||||
* If cli.markdown does not require updating and the developer cannot run
|
* 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
|
* `npm run build` on their laptop, the error message suggests a workaround
|
||||||
* using `touch`.
|
* using `touch`.
|
||||||
*/
|
*/
|
||||||
async function checkBuildTimestamps() {
|
async function checkBuildTimestamps() {
|
||||||
const git = simplegit(ROOT);
|
const git = simpleGit(ROOT);
|
||||||
const docFile = path.join(ROOT, 'doc', 'cli.markdown');
|
const docFile = path.join(ROOT, 'docs', 'balena-cli.md');
|
||||||
const [docStat, gitStatus] = await Promise.all([
|
const [docStat, gitStatus] = await Promise.all([
|
||||||
fs.stat(docFile),
|
fs.stat(docFile),
|
||||||
git.status(),
|
git.status(),
|
||||||
@ -81,4 +82,5 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
run();
|
run();
|
@ -6,6 +6,8 @@
|
|||||||
*
|
*
|
||||||
* We don't `require('semver')` to allow this script to be run as a npm
|
* 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.
|
* 'preinstall' hook, at which point no dependencies have been installed.
|
||||||
|
*
|
||||||
|
* @param {string} version
|
||||||
*/
|
*/
|
||||||
function parseSemver(version) {
|
function parseSemver(version) {
|
||||||
const match = /v?(\d+)\.(\d+).(\d+)/.exec(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)];
|
return [parseInt(major, 10), parseInt(minor, 10), parseInt(patch, 10)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} v1
|
||||||
|
* @param {string} v2
|
||||||
|
*/
|
||||||
function semverGte(v1, v2) {
|
function semverGte(v1, v2) {
|
||||||
let v1Array = parseSemver(v1);
|
const v1Array = parseSemver(v1);
|
||||||
let v2Array = parseSemver(v2);
|
const v2Array = parseSemver(v2);
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
if (v1Array[i] < v2Array[i]) {
|
if (v1Array[i] < v2Array[i]) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -30,7 +30,7 @@ const { GITHUB_TOKEN } = process.env;
|
|||||||
export async function createGitHubRelease() {
|
export async function createGitHubRelease() {
|
||||||
console.log(`Publishing release ${version} to GitHub`);
|
console.log(`Publishing release ${version} to GitHub`);
|
||||||
const publishRelease = await import('publish-release');
|
const publishRelease = await import('publish-release');
|
||||||
const ghRelease = await Bluebird.fromCallback(
|
const ghRelease = (await Bluebird.fromCallback(
|
||||||
publishRelease.bind(null, {
|
publishRelease.bind(null, {
|
||||||
token: GITHUB_TOKEN || '',
|
token: GITHUB_TOKEN || '',
|
||||||
owner: 'balena-io',
|
owner: 'balena-io',
|
||||||
@ -40,7 +40,7 @@ export async function createGitHubRelease() {
|
|||||||
reuseRelease: true,
|
reuseRelease: true,
|
||||||
assets: finalReleaseAssets[process.platform],
|
assets: finalReleaseAssets[process.platform],
|
||||||
}),
|
}),
|
||||||
);
|
)) as { html_url: any };
|
||||||
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
console.log(`Release ${version} successful: ${ghRelease.html_url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ async function updateGitHubReleaseDescriptions(
|
|||||||
) {
|
) {
|
||||||
const perPage = 30;
|
const perPage = 30;
|
||||||
const octokit = getOctokit();
|
const octokit = getOctokit();
|
||||||
const options = await octokit.repos.listReleases.endpoint.merge({
|
const options = octokit.repos.listReleases.endpoint.merge({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
per_page: perPage,
|
per_page: perPage,
|
||||||
|
@ -60,7 +60,7 @@ async function parse(args?: string[]) {
|
|||||||
release,
|
release,
|
||||||
};
|
};
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
if (!commands.hasOwnProperty(arg)) {
|
if (!Object.hasOwn(commands, arg)) {
|
||||||
throw new Error(`command unknown: ${arg}`);
|
throw new Error(`command unknown: ${arg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,4 +103,5 @@ export async function run(args?: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
run();
|
run();
|
||||||
|
@ -36,8 +36,8 @@ const run = async (cmd: string) => {
|
|||||||
}
|
}
|
||||||
resolve({ stdout, stderr });
|
resolve({ stdout, stderr });
|
||||||
});
|
});
|
||||||
p.stdout.pipe(process.stdout);
|
p.stdout?.pipe(process.stdout);
|
||||||
p.stderr.pipe(process.stderr);
|
p.stderr?.pipe(process.stderr);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,4 +136,5 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
main();
|
main();
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
export const ROOT = path.join(__dirname, '..');
|
export const ROOT = path.join(__dirname, '..');
|
||||||
@ -117,7 +116,7 @@ export async function which(program: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export async function whichSpawn(
|
export async function whichSpawn(
|
||||||
programName: string,
|
programName: string,
|
||||||
args?: string[],
|
args: string[] = [],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const program = await which(programName);
|
const program = await which(programName);
|
||||||
let error: Error | undefined;
|
let error: Error | undefined;
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
// tslint:disable:no-var-requires
|
|
||||||
|
|
||||||
// We boost the threadpool size as ext2fs can deadlock with some
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
// operations otherwise, if the pool runs out.
|
// operations otherwise, if the pool runs out.
|
||||||
process.env.UV_THREADPOOL_SIZE = '64';
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
@ -17,7 +15,7 @@ async function run() {
|
|||||||
require('@balena/es-version').set('es2018');
|
require('@balena/es-version').set('es2018');
|
||||||
|
|
||||||
// Run the CLI
|
// Run the CLI
|
||||||
await require('../build/app').run();
|
await require('../build/app').run(undefined, { dir: __dirname });
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
// Before opening a PR you should build and test your changes using bin/balena
|
// 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
|
// We boost the threadpool size as ext2fs can deadlock with some
|
||||||
// operations otherwise, if the pool runs out.
|
// operations otherwise, if the pool runs out.
|
||||||
process.env.UV_THREADPOOL_SIZE = '64';
|
process.env.UV_THREADPOOL_SIZE = '64';
|
||||||
@ -59,7 +57,7 @@ require('ts-node').register({
|
|||||||
project: path.join(rootDir, 'tsconfig.json'),
|
project: path.join(rootDir, 'tsconfig.json'),
|
||||||
transpileOnly: true,
|
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
|
// Modify package.json oclif paths from build/ -> lib/, or vice versa
|
||||||
function modifyOclifPaths(revert) {
|
function modifyOclifPaths(revert) {
|
||||||
|
@ -8,19 +8,21 @@ _balena() {
|
|||||||
local context state line curcontext="$curcontext"
|
local context state line curcontext="$curcontext"
|
||||||
|
|
||||||
# Valid top-level completions
|
# Valid top-level completions
|
||||||
main_commands=( apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util )
|
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
|
# Sub-completions
|
||||||
api_key_cmds=( generate )
|
api_key_cmds=( generate revoke )
|
||||||
app_cmds=( create purge rename restart rm )
|
app_cmds=( create )
|
||||||
|
block_cmds=( create )
|
||||||
config_cmds=( generate inject read reconfigure write )
|
config_cmds=( generate inject read reconfigure write )
|
||||||
device_cmds=( deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown )
|
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 )
|
devices_cmds=( supported )
|
||||||
env_cmds=( add rename rm )
|
env_cmds=( add rename rm )
|
||||||
fleet_cmds=( create purge rename restart rm )
|
fleet_cmds=( create pin purge rename restart rm track-latest )
|
||||||
internal_cmds=( osinit )
|
internal_cmds=( osinit )
|
||||||
key_cmds=( add rm )
|
key_cmds=( add rm )
|
||||||
local_cmds=( configure flash )
|
local_cmds=( configure flash )
|
||||||
os_cmds=( build-config configure download initialize versions )
|
os_cmds=( build-config configure download initialize versions )
|
||||||
|
release_cmds=( finalize invalidate validate )
|
||||||
tag_cmds=( rm set )
|
tag_cmds=( rm set )
|
||||||
|
|
||||||
|
|
||||||
@ -46,6 +48,9 @@ _balena_sec_cmds() {
|
|||||||
"app")
|
"app")
|
||||||
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
_describe -t app_cmds 'app_cmd' app_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
"block")
|
||||||
|
_describe -t block_cmds 'block_cmd' block_cmds "$@" && ret=0
|
||||||
|
;;
|
||||||
"config")
|
"config")
|
||||||
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
_describe -t config_cmds 'config_cmd' config_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
@ -73,6 +78,9 @@ _balena_sec_cmds() {
|
|||||||
"os")
|
"os")
|
||||||
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
_describe -t os_cmds 'os_cmd' os_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
"release")
|
||||||
|
_describe -t release_cmds 'release_cmd' release_cmds "$@" && ret=0
|
||||||
|
;;
|
||||||
"tag")
|
"tag")
|
||||||
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
_describe -t tag_cmds 'tag_cmd' tag_cmds "$@" && ret=0
|
||||||
;;
|
;;
|
||||||
|
@ -7,19 +7,21 @@ _balena_complete()
|
|||||||
local cur prev
|
local cur prev
|
||||||
|
|
||||||
# Valid top-level completions
|
# Valid top-level completions
|
||||||
main_commands="apps build deploy envs fleets join keys leave login logout logs note orgs preload push scan settings ssh support tags tunnel version whoami api-key app app config device device devices env fleet fleet internal key key local os tag util"
|
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
|
# Sub-completions
|
||||||
api_key_cmds="generate"
|
api_key_cmds="generate revoke"
|
||||||
app_cmds="create purge rename restart rm"
|
app_cmds="create"
|
||||||
|
block_cmds="create"
|
||||||
config_cmds="generate inject read reconfigure write"
|
config_cmds="generate inject read reconfigure write"
|
||||||
device_cmds="deactivate identify init local-mode move os-update public-url purge reboot register rename restart rm shutdown"
|
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"
|
devices_cmds="supported"
|
||||||
env_cmds="add rename rm"
|
env_cmds="add rename rm"
|
||||||
fleet_cmds="create purge rename restart rm"
|
fleet_cmds="create pin purge rename restart rm track-latest"
|
||||||
internal_cmds="osinit"
|
internal_cmds="osinit"
|
||||||
key_cmds="add rm"
|
key_cmds="add rm"
|
||||||
local_cmds="configure flash"
|
local_cmds="configure flash"
|
||||||
os_cmds="build-config configure download initialize versions"
|
os_cmds="build-config configure download initialize versions"
|
||||||
|
release_cmds="finalize invalidate validate"
|
||||||
tag_cmds="rm set"
|
tag_cmds="rm set"
|
||||||
|
|
||||||
|
|
||||||
@ -40,6 +42,9 @@ _balena_complete()
|
|||||||
app)
|
app)
|
||||||
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
block)
|
||||||
|
COMPREPLY=( $(compgen -W "$block_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
config)
|
config)
|
||||||
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
@ -67,6 +72,9 @@ _balena_complete()
|
|||||||
os)
|
os)
|
||||||
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
release)
|
||||||
|
COMPREPLY=( $(compgen -W "$release_cmds" -- $cur) )
|
||||||
|
;;
|
||||||
tag)
|
tag)
|
||||||
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
COMPREPLY=( $(compgen -W "$tag_cmds" -- $cur) )
|
||||||
;;
|
;;
|
||||||
|
@ -31,8 +31,8 @@ if (fs.existsSync(commandsFilePath)) {
|
|||||||
|
|
||||||
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
const commandsJson = JSON.parse(fs.readFileSync(commandsFilePath, 'utf8'));
|
||||||
|
|
||||||
var mainCommands = [];
|
const mainCommands = [];
|
||||||
var additionalCommands = [];
|
const additionalCommands = [];
|
||||||
for (const key of Object.keys(commandsJson.commands)) {
|
for (const key of Object.keys(commandsJson.commands)) {
|
||||||
const cmd = key.split(':');
|
const cmd = key.split(':');
|
||||||
if (cmd.length > 1) {
|
if (cmd.length > 1) {
|
||||||
@ -72,8 +72,8 @@ fs.readFile(bashFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands="' + mainCommandsStr + '"',
|
'main_commands="' + mainCommandsStr + '"',
|
||||||
);
|
);
|
||||||
var subCommands = [];
|
let subCommands = [];
|
||||||
var prevElement = additionalCommands[0][0];
|
let prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
@ -134,8 +134,8 @@ fs.readFile(zshFilePathIn, 'utf8', function (err, data) {
|
|||||||
/\$main_commands\$/g,
|
/\$main_commands\$/g,
|
||||||
'main_commands=( ' + mainCommandsStr + ' )',
|
'main_commands=( ' + mainCommandsStr + ' )',
|
||||||
);
|
);
|
||||||
var subCommands = [];
|
let subCommands = [];
|
||||||
var prevElement = additionalCommands[0][0];
|
let prevElement = additionalCommands[0][0];
|
||||||
additionalCommands.forEach(function (element) {
|
additionalCommands.forEach(function (element) {
|
||||||
if (element[0] === prevElement) {
|
if (element[0] === prevElement) {
|
||||||
subCommands.push(element[1]);
|
subCommands.push(element[1]);
|
||||||
|
@ -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 --fleet FLEET_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. `FLEET_ID`. Create a fleet (`balena fleet create FLEET_NAME --type DEVICE_TYPE`) or find an existing one (`balena fleets`) 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')),
|
|
||||||
);
|
|
33
lib/app.ts
33
lib/app.ts
@ -24,6 +24,7 @@ import {
|
|||||||
} from './preparser';
|
} from './preparser';
|
||||||
import { CliSettings } from './utils/bootstrap';
|
import { CliSettings } from './utils/bootstrap';
|
||||||
import { onceAsync } from './utils/lazy';
|
import { onceAsync } from './utils/lazy';
|
||||||
|
import { run as mainRun, settings } from '@oclif/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry.io setup
|
* Sentry.io setup
|
||||||
@ -33,6 +34,7 @@ export const setupSentry = onceAsync(async () => {
|
|||||||
const config = await import('./config');
|
const config = await import('./config');
|
||||||
const Sentry = await import('@sentry/node');
|
const Sentry = await import('@sentry/node');
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
|
autoSessionTracking: false,
|
||||||
dsn: config.sentryDsn,
|
dsn: config.sentryDsn,
|
||||||
release: packageJSON.version,
|
release: packageJSON.version,
|
||||||
});
|
});
|
||||||
@ -76,11 +78,13 @@ export function setMaxListeners(maxListeners: number) {
|
|||||||
/** Selected CLI initialization steps */
|
/** Selected CLI initialization steps */
|
||||||
async function init() {
|
async function init() {
|
||||||
if (process.env.BALENARC_NO_SENTRY) {
|
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 {
|
} else {
|
||||||
await setupSentry();
|
await setupSentry();
|
||||||
}
|
}
|
||||||
checkNodeVersion();
|
await checkNodeVersion();
|
||||||
|
|
||||||
const settings = new CliSettings();
|
const settings = new CliSettings();
|
||||||
|
|
||||||
@ -90,14 +94,16 @@ async function init() {
|
|||||||
setupBalenaSdkSharedOptions(settings);
|
setupBalenaSdkSharedOptions(settings);
|
||||||
|
|
||||||
// check for CLI updates once a day
|
// 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. */
|
/** Execute the oclif parser and the CLI command. */
|
||||||
async function oclifRun(command: string[], options: AppOptions) {
|
async function oclifRun(command: string[], options: AppOptions) {
|
||||||
let deprecationPromise: Promise<void>;
|
let deprecationPromise: Promise<void>;
|
||||||
// check and enforce the CLI's deprecation policy
|
// check and enforce the CLI's deprecation policy
|
||||||
if (unsupportedFlag) {
|
if (unsupportedFlag || process.env.BALENARC_UNSUPPORTED) {
|
||||||
deprecationPromise = Promise.resolve();
|
deprecationPromise = Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
const { DeprecationChecker } = await import('./deprecation');
|
const { DeprecationChecker } = await import('./deprecation');
|
||||||
@ -109,10 +115,16 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runPromise = (async function (shouldFlush: boolean) {
|
const runPromise = (async function (shouldFlush: boolean) {
|
||||||
const { CustomMain } = await import('./utils/oclif-utils');
|
|
||||||
let isEEXIT = false;
|
let isEEXIT = false;
|
||||||
try {
|
try {
|
||||||
await CustomMain.run(command);
|
if (options.development) {
|
||||||
|
// In dev mode -> use ts-node and dev plugins
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
settings.debug = true;
|
||||||
|
}
|
||||||
|
// 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) {
|
} catch (error) {
|
||||||
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
// oclif sometimes exits with ExitError code EEXIT 0 (not an error),
|
||||||
// for example the `balena help` command.
|
// for example the `balena help` command.
|
||||||
@ -125,7 +137,7 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shouldFlush) {
|
if (shouldFlush) {
|
||||||
await import('@oclif/command/flush');
|
await import('@oclif/core/flush');
|
||||||
}
|
}
|
||||||
// TODO: figure out why we need to call fast-boot stop() here, in
|
// 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.
|
// addition to calling it in the main `run()` function in this file.
|
||||||
@ -146,9 +158,12 @@ async function oclifRun(command: string[], options: AppOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
/** CLI entrypoint. Called by the `bin/balena` and `bin/balena-dev` scripts. */
|
||||||
export async function run(cliArgs = process.argv, options: AppOptions = {}) {
|
export async function run(cliArgs = process.argv, options: AppOptions) {
|
||||||
try {
|
try {
|
||||||
const { normalizeEnvVars, pkgExec } = await import('./utils/bootstrap');
|
const { setOfflineModeEnvVars, normalizeEnvVars, pkgExec } = await import(
|
||||||
|
'./utils/bootstrap'
|
||||||
|
);
|
||||||
|
setOfflineModeEnvVars();
|
||||||
normalizeEnvVars();
|
normalizeEnvVars();
|
||||||
|
|
||||||
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
// The 'pkgExec' special/internal command provides a Node.js interpreter
|
||||||
|
@ -56,7 +56,7 @@ export async function login({ host = '127.0.0.1', port = 0 }) {
|
|||||||
|
|
||||||
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
console.info(`Opening web browser for URL:\n${loginUrl}`);
|
||||||
const open = await import('open');
|
const open = await import('open');
|
||||||
open(loginUrl, { wait: false });
|
await open(loginUrl, { wait: false });
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const token = await loginServer.awaitForToken();
|
const token = await loginServer.awaitForToken();
|
||||||
|
@ -14,57 +14,44 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as url from 'url';
|
|
||||||
import { getBalenaSdk } from '../utils/lazy';
|
import { getBalenaSdk } from '../utils/lazy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get dashboard CLI login URL
|
* Get dashboard CLI login URL
|
||||||
* @function
|
|
||||||
* @protected
|
|
||||||
*
|
*
|
||||||
* @param {String} callbackUrl - callback url
|
* @param callbackUrl - Callback url, e.g. 'http://127.0.0.1:3000'
|
||||||
* @fulfil {String} - dashboard login url
|
* @returns Dashboard login URL, e.g.:
|
||||||
* @returns {Promise}
|
* 'https://dashboard.balena-cloud.com/login/cli/http%253A%252F%252F127.0.0.1%253A59581%252Fauth'
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* utils.getDashboardLoginURL('http://127.0.0.1:3000').then (url) ->
|
|
||||||
* console.log(url)
|
|
||||||
*/
|
*/
|
||||||
export const getDashboardLoginURL = (callbackUrl: string) => {
|
export async function getDashboardLoginURL(
|
||||||
|
callbackUrl: string,
|
||||||
|
): Promise<string> {
|
||||||
// Encode percentages signs from the escaped url
|
// Encode percentages signs from the escaped url
|
||||||
// characters to avoid angular getting confused.
|
// characters to avoid angular getting confused.
|
||||||
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
|
callbackUrl = encodeURIComponent(callbackUrl).replace(/%/g, '%25');
|
||||||
|
|
||||||
return getBalenaSdk()
|
const [{ URL }, dashboardUrl] = await Promise.all([
|
||||||
.settings.get('dashboardUrl')
|
import('url'),
|
||||||
.then((dashboardUrl) =>
|
getBalenaSdk().settings.get('dashboardUrl'),
|
||||||
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
|
]);
|
||||||
);
|
return new URL(`/login/cli/${callbackUrl}`, dashboardUrl).href;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Log in using a token, but only if the token is valid
|
* Log in using a token, but only if the token is valid.
|
||||||
* @function
|
|
||||||
* @protected
|
|
||||||
*
|
*
|
||||||
* @description
|
|
||||||
* This function checks that the token is not only well-structured
|
* This function checks that the token is not only well-structured
|
||||||
* but that it also authenticates with the server successfully.
|
* but that it also authenticates with the server successfully.
|
||||||
*
|
*
|
||||||
* If authenticated, the token is persisted, if not then the previous
|
* If authenticated, the token is persisted, if not then the previous
|
||||||
* login state is restored.
|
* login state is restored.
|
||||||
*
|
*
|
||||||
* @param {String} token - session token or api key
|
* @param token - session token or api key
|
||||||
* @fulfil {Boolean} - whether the login was successful or not
|
* @returns whether the login was successful or not
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* utils.loginIfTokenValid('...').then (loggedIn) ->
|
|
||||||
* if loggedIn
|
|
||||||
* console.log('Token is valid!')
|
|
||||||
*/
|
*/
|
||||||
export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
export async function loginIfTokenValid(token?: string): Promise<boolean> {
|
||||||
if (_.isEmpty(token?.trim())) {
|
token = (token || '').trim();
|
||||||
|
if (!token) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
@ -86,4 +73,4 @@ export const loginIfTokenValid = async (token: string): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return isLoggedIn;
|
return isLoggedIn;
|
||||||
};
|
}
|
||||||
|
@ -15,8 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Command from '@oclif/command';
|
import { Command } from '@oclif/core';
|
||||||
import { InsufficientPrivilegesError } from './errors';
|
import {
|
||||||
|
InsufficientPrivilegesError,
|
||||||
|
NotAvailableInOfflineModeError,
|
||||||
|
} from './errors';
|
||||||
|
import { stripIndent } from './utils/lazy';
|
||||||
|
import * as output from './framework/output';
|
||||||
|
|
||||||
export default abstract class BalenaCommand extends Command {
|
export default abstract class BalenaCommand extends Command {
|
||||||
/**
|
/**
|
||||||
@ -40,6 +45,13 @@ export default abstract class BalenaCommand extends Command {
|
|||||||
*/
|
*/
|
||||||
public static authenticated = false;
|
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.
|
* Accept piped input.
|
||||||
* When set to true, command will read from stdin during init
|
* 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.
|
* Read stdin contents and make available to command.
|
||||||
*
|
*
|
||||||
@ -125,8 +160,15 @@ export default abstract class BalenaCommand extends Command {
|
|||||||
await BalenaCommand.checkLoggedIn();
|
await BalenaCommand.checkLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ctr.offlineCompatible) {
|
||||||
|
BalenaCommand.checkNotUsingOfflineMode();
|
||||||
|
}
|
||||||
|
|
||||||
if (ctr.readStdin) {
|
if (ctr.readStdin) {
|
||||||
await this.getStdin();
|
await this.getStdin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected outputMessage = output.outputMessage;
|
||||||
|
protected outputData = output.outputData;
|
||||||
}
|
}
|
||||||
|
@ -15,20 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GenerateCmd extends Command {
|
export default class GenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Generate a new balenaCloud API key.
|
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 examples = ['$ balena api-key generate "Jenkins Key"'];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
|
||||||
description: 'the API key name',
|
description: 'the API key name',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'api-key generate <name>';
|
public static usage = 'api-key generate <name>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(GenerateCmd);
|
const { args: params } = await this.parse(GenerateCmd);
|
||||||
|
|
||||||
let key;
|
let key;
|
||||||
try {
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,38 +15,24 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
import type { Application } from 'balena-sdk';
|
|
||||||
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
import { appToFleetCmdMsg, warnify } from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
export default class AppCreateCmd extends Command {
|
||||||
organization?: string;
|
|
||||||
type?: string; // application device type
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FleetCreateCmd extends Command {
|
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Create a fleet.
|
Create an app.
|
||||||
|
|
||||||
Create a new balena fleet.
|
Create a new balena app.
|
||||||
|
|
||||||
You can specify the organization the fleet should belong to using
|
You can specify the organization the app should belong to using
|
||||||
the \`--organization\` option. The organization's handle, not its name,
|
the \`--organization\` option. The organization's handle, not its name,
|
||||||
should be provided. Organization handles can be listed with the
|
should be provided. Organization handles can be listed with the
|
||||||
\`balena orgs\` command.
|
\`balena orgs\` command.
|
||||||
|
|
||||||
The fleet's default device type is specified with the \`--type\` option.
|
The app's default device type is specified with the \`--type\` option.
|
||||||
The \`balena devices supported\` command can be used to list the available
|
The \`balena devices supported\` command can be used to list the available
|
||||||
device types.
|
device types.
|
||||||
|
|
||||||
@ -58,124 +44,40 @@ export class FleetCreateCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena fleet create MyFleet',
|
'$ balena app create MyApp',
|
||||||
'$ balena fleet create MyFleet --organization mmyorg',
|
'$ balena app create MyApp --organization mmyorg',
|
||||||
'$ balena fleet create MyFleet -o myorg --type raspberry-pi',
|
'$ balena app create MyApp -o myorg --type raspberry-pi',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
description: 'app name',
|
||||||
description: 'fleet name',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'fleet create <name>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
organization: flags.string({
|
|
||||||
char: 'o',
|
|
||||||
description: 'handle of the organization the fleet should belong to',
|
|
||||||
}),
|
}),
|
||||||
type: flags.string({
|
};
|
||||||
|
|
||||||
|
public static usage = 'app create <name>';
|
||||||
|
|
||||||
|
public static flags = {
|
||||||
|
organization: Flags.string({
|
||||||
|
char: 'o',
|
||||||
|
description: 'handle of the organization the app should belong to',
|
||||||
|
}),
|
||||||
|
type: Flags.string({
|
||||||
char: 't',
|
char: 't',
|
||||||
description:
|
description:
|
||||||
'fleet device type (Check available types with `balena devices supported`)',
|
'app device type (Check available types with `balena devices supported`)',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params, flags: options } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCreateCmd);
|
|
||||||
|
|
||||||
// Ascertain device type
|
|
||||||
const deviceType =
|
|
||||||
options.type ||
|
|
||||||
(await (await import('../../utils/patterns')).selectDeviceType());
|
|
||||||
|
|
||||||
// Ascertain organization
|
|
||||||
const organization =
|
|
||||||
options.organization?.toLowerCase() || (await this.getOrganization());
|
|
||||||
|
|
||||||
// Create application
|
|
||||||
let application: Application;
|
|
||||||
try {
|
|
||||||
application = await getBalenaSdk().models.application.create({
|
|
||||||
name: params.name,
|
|
||||||
deviceType,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if ((err.message || '').toLowerCase().includes('unique')) {
|
|
||||||
// BalenaRequestError: Request error: "organization" and "app_name" must be unique.
|
|
||||||
throw new ExpectedError(
|
|
||||||
`Error: fleet "${params.name}" already exists in organization "${organization}".`,
|
|
||||||
);
|
|
||||||
} else if ((err.message || '').toLowerCase().includes('unauthorized')) {
|
|
||||||
// BalenaRequestError: Request error: Unauthorized
|
|
||||||
throw new ExpectedError(
|
|
||||||
`Error: You are not authorized to create fleets in organization "${organization}".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output
|
|
||||||
const { isV13 } = await import('../../utils/version');
|
|
||||||
console.log(
|
|
||||||
isV13()
|
|
||||||
? `Fleet created: slug "${application.slug}", device type "${deviceType}"`
|
|
||||||
: `Fleet created: ${application.slug} (${deviceType}, id ${application.id})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrganization() {
|
|
||||||
const { getOwnOrganizations } = await import('../../utils/sdk');
|
|
||||||
const organizations = await getOwnOrganizations(getBalenaSdk());
|
|
||||||
|
|
||||||
if (organizations.length === 0) {
|
|
||||||
// User is not a member of any organizations (should not happen).
|
|
||||||
throw new Error('This account is not a member of any organizations');
|
|
||||||
} else if (organizations.length === 1) {
|
|
||||||
// User is a member of only one organization - use this.
|
|
||||||
return organizations[0].handle;
|
|
||||||
} else {
|
|
||||||
// User is a member of multiple organizations -
|
|
||||||
const { selectOrganization } = await import('../../utils/patterns');
|
|
||||||
return selectOrganization(organizations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppCreateCmd extends FleetCreateCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet create' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet create'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app create <name>';
|
|
||||||
public static args = FleetCreateCmd.args;
|
|
||||||
public static flags = FleetCreateCmd.flags;
|
|
||||||
public static authenticated = FleetCreateCmd.authenticated;
|
|
||||||
public static primary = FleetCreateCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
const { args: params, flags: options } = await this.parse(AppCreateCmd);
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCreateCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
await (
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
await import('../../utils/application-create')
|
||||||
}
|
).applicationCreateBase('app', options, params);
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,124 +0,0 @@
|
|||||||
/**
|
|
||||||
* @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 { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
import type { Release } from 'balena-sdk';
|
|
||||||
|
|
||||||
import Command from '../../command';
|
|
||||||
import * as cf from '../../utils/common-flags';
|
|
||||||
import * as ca from '../../utils/common-args';
|
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
|
||||||
import {
|
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetCmdMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static args = [ca.fleetRequired];
|
|
||||||
|
|
||||||
public static usage = 'fleet <fleet>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
public static primary = true;
|
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
|
|
||||||
const application = (await getApplication(getBalenaSdk(), params.fleet, {
|
|
||||||
$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',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppCmd extends FleetCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app <fleet>';
|
|
||||||
public static args = FleetCmd.args;
|
|
||||||
public static flags = FleetCmd.flags;
|
|
||||||
public static authenticated = FleetCmd.authenticated;
|
|
||||||
public static primary = FleetCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
|
||||||
}
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,115 +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 type { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
|
|
||||||
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,
|
|
||||||
appToFleetCmdMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FleetPurgeCmd extends Command {
|
|
||||||
public static description = stripIndent`
|
|
||||||
Purge data from a fleet.
|
|
||||||
|
|
||||||
Purge data from all devices belonging to a fleet.
|
|
||||||
This will clear the fleet's '/data' directory.
|
|
||||||
|
|
||||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static examples = [
|
|
||||||
'$ balena fleet purge MyFleet',
|
|
||||||
'$ balena fleet purge myorg/myfleet',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static args = [ca.fleetRequired];
|
|
||||||
|
|
||||||
public static usage = 'fleet purge <fleet>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(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,
|
|
||||||
const application = await getApplication(balena, params.fleet);
|
|
||||||
|
|
||||||
try {
|
|
||||||
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
|
|
||||||
// ignore in this case.
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppPurgeCmd extends FleetPurgeCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet purge' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet purge'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app purge <fleet>';
|
|
||||||
public static args = FleetPurgeCmd.args;
|
|
||||||
public static flags = FleetPurgeCmd.flags;
|
|
||||||
public static authenticated = FleetPurgeCmd.authenticated;
|
|
||||||
public static primary = FleetPurgeCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppPurgeCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
|
||||||
}
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,170 +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 type { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
import type { ApplicationType } from 'balena-sdk';
|
|
||||||
|
|
||||||
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,
|
|
||||||
appToFleetCmdMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
newName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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 = [
|
|
||||||
ca.fleetRequired,
|
|
||||||
{
|
|
||||||
name: 'newName',
|
|
||||||
description: 'the new name for the fleet',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'fleet rename <fleet> [newName]';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(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, {
|
|
||||||
$expand: {
|
|
||||||
application_type: {
|
|
||||||
$select: ['is_legacy'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check app exists
|
|
||||||
if (!application) {
|
|
||||||
throw new ExpectedError(`Error: fleet ${params.fleet} not found.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check app supports renaming
|
|
||||||
const appType = (application.application_type as ApplicationType[])?.[0];
|
|
||||||
if (appType.is_legacy) {
|
|
||||||
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,
|
|
||||||
})) ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
// 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 ${params.fleet} already exists.`);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get application again, to be sure of results
|
|
||||||
const renamedApplication = await balena.models.application.get(
|
|
||||||
application.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppRenameCmd extends FleetRenameCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet rename' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet rename'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app rename <fleet> [newName]';
|
|
||||||
public static args = FleetRenameCmd.args;
|
|
||||||
public static flags = FleetRenameCmd.flags;
|
|
||||||
public static authenticated = FleetRenameCmd.authenticated;
|
|
||||||
public static primary = FleetRenameCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRenameCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
|
||||||
}
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +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 type { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
|
|
||||||
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,
|
|
||||||
appToFleetCmdMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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 = [ca.fleetRequired];
|
|
||||||
|
|
||||||
public static usage = 'fleet restart <fleet>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
help: cf.help,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRestartCmd);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
|
||||||
|
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
|
||||||
const application = await getApplication(balena, params.fleet);
|
|
||||||
|
|
||||||
await balena.models.application.restart(application.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppRestartCmd extends FleetRestartCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet restart' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet restart'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app restart <fleet>';
|
|
||||||
public static args = FleetRestartCmd.args;
|
|
||||||
public static flags = FleetRestartCmd.flags;
|
|
||||||
public static authenticated = FleetRestartCmd.authenticated;
|
|
||||||
public static primary = FleetRestartCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRestartCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
|
||||||
}
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,116 +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 type { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
|
|
||||||
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,
|
|
||||||
appToFleetCmdMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FleetRmCmd extends Command {
|
|
||||||
public static description = stripIndent`
|
|
||||||
Remove a fleet.
|
|
||||||
|
|
||||||
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 fleet rm MyFleet',
|
|
||||||
'$ balena fleet rm MyFleet --yes',
|
|
||||||
'$ balena fleet rm myorg/myfleet',
|
|
||||||
];
|
|
||||||
|
|
||||||
public static args = [ca.fleetRequired];
|
|
||||||
|
|
||||||
public static usage = 'fleet rm <fleet>';
|
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
|
||||||
yes: cf.yes,
|
|
||||||
help: cf.help,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
|
||||||
const { args: params, flags: options } =
|
|
||||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetRmCmd);
|
|
||||||
|
|
||||||
const { confirm } = await import('../../utils/patterns');
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
const balena = getBalenaSdk();
|
|
||||||
|
|
||||||
// Confirm
|
|
||||||
await confirm(
|
|
||||||
options.yes ?? false,
|
|
||||||
`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);
|
|
||||||
|
|
||||||
// Remove
|
|
||||||
await balena.models.application.remove(application.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AppRmCmd extends FleetRmCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleet rm' command
|
|
||||||
|
|
||||||
${appToFleetCmdMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleet rm'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'app rm <fleet>';
|
|
||||||
public static args = FleetRmCmd.args;
|
|
||||||
public static flags = FleetRmCmd.flags;
|
|
||||||
public static authenticated = FleetRmCmd.authenticated;
|
|
||||||
public static primary = FleetRmCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, ArgsDef>(AppRmCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetCmdMsg));
|
|
||||||
}
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2016-2021 Balena Ltd.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
|
||||||
import type { Output as ParserOutput } from '@oclif/parser';
|
|
||||||
|
|
||||||
import Command from '../command';
|
|
||||||
import * as cf from '../utils/common-flags';
|
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
|
||||||
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
|
||||||
|
|
||||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
|
||||||
device_count?: number;
|
|
||||||
online_devices?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
verbose?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export 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: 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;
|
|
||||||
|
|
||||||
protected useAppWord = false;
|
|
||||||
|
|
||||||
public async run(_parserOutput?: ParserOutput<FlagsDef, {}>) {
|
|
||||||
_parserOutput ||= this.parse<FlagsDef, {}>(FleetsCmd);
|
|
||||||
|
|
||||||
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',
|
|
||||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
|
||||||
'slug',
|
|
||||||
'device_type',
|
|
||||||
'online_devices',
|
|
||||||
'device_count',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const appsToFleetsRenameMsg = appToFleetCmdMsg
|
|
||||||
.replace(/'app'/g, "'apps'")
|
|
||||||
.replace(/'fleet'/g, "'fleets'");
|
|
||||||
|
|
||||||
export default class AppsCmd extends FleetsCmd {
|
|
||||||
public static description = stripIndent`
|
|
||||||
DEPRECATED alias for the 'fleets' command
|
|
||||||
|
|
||||||
${appsToFleetsRenameMsg
|
|
||||||
.split('\n')
|
|
||||||
.map((l) => `\t\t${l}`)
|
|
||||||
.join('\n')}
|
|
||||||
|
|
||||||
For command usage, see 'balena help fleets'
|
|
||||||
`;
|
|
||||||
public static examples = [];
|
|
||||||
public static usage = 'apps';
|
|
||||||
public static args = FleetsCmd.args;
|
|
||||||
public static flags = FleetsCmd.flags;
|
|
||||||
public static authenticated = FleetsCmd.authenticated;
|
|
||||||
public static primary = FleetsCmd.primary;
|
|
||||||
|
|
||||||
public async run() {
|
|
||||||
// call this.parse() before deprecation message to parse '-h'
|
|
||||||
const parserOutput = this.parse<FlagsDef, {}>(AppsCmd);
|
|
||||||
if (process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appsToFleetsRenameMsg));
|
|
||||||
}
|
|
||||||
this.useAppWord = true;
|
|
||||||
await super.run(parserOutput);
|
|
||||||
}
|
|
||||||
}
|
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -15,38 +15,33 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args, Flags } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { getBalenaSdk } from '../utils/lazy';
|
import { getBalenaSdk } from '../../utils/lazy';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../../utils/compose';
|
||||||
import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk';
|
import type { ApplicationType, BalenaSDK } from 'balena-sdk';
|
||||||
import {
|
import {
|
||||||
appToFleetFlagMsg,
|
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
warnify,
|
} from '../../utils/messages';
|
||||||
} from '../utils/messages';
|
import type { ComposeCliFlags, ComposeOpts } from '../../utils/compose-types';
|
||||||
import type { ComposeCliFlags, ComposeOpts } from '../utils/compose-types';
|
import { buildProject, composeCliFlags } from '../../utils/compose_ts';
|
||||||
import { buildProject, composeCliFlags } from '../utils/compose_ts';
|
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
import { dockerCliFlags } from '../../utils/docker';
|
||||||
import { dockerCliFlags } from '../utils/docker';
|
|
||||||
import { isV13 } from '../utils/version';
|
|
||||||
|
|
||||||
|
// 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 {
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
arch?: string;
|
arch?: string;
|
||||||
deviceType?: string;
|
deviceType?: string;
|
||||||
application?: string;
|
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
source?: string; // Not part of command profile - source param copied here.
|
source?: string; // Not part of command profile - source param copied here.
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
source?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class BuildCmd extends Command {
|
export default class BuildCmd extends Command {
|
||||||
public static description = `\
|
public static description = `\
|
||||||
Build a project locally.
|
Build a project locally.
|
||||||
@ -78,46 +73,35 @@ ${dockerignoreHelp}
|
|||||||
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
'$ balena build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem -f myFleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
source: Args.string({ description: 'path of project source directory' }),
|
||||||
name: 'source',
|
};
|
||||||
description: 'path of project source directory',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'build [source]';
|
public static usage = 'build [source]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
arch: flags.string({
|
arch: Flags.string({
|
||||||
description: 'the architecture to build for',
|
description: 'the architecture to build for',
|
||||||
char: 'A',
|
char: 'A',
|
||||||
}),
|
}),
|
||||||
deviceType: flags.string({
|
deviceType: Flags.string({
|
||||||
description: 'the type of device this build is for',
|
description: 'the type of device this build is for',
|
||||||
char: 'd',
|
char: 'd',
|
||||||
}),
|
}),
|
||||||
...(isV13() ? {} : { application: cf.application }),
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
// Revisit this in future release.
|
// Revisit this in future release.
|
||||||
help: flags.help({}),
|
help: Flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(BuildCmd);
|
||||||
BuildCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options.application && process.stderr.isTTY) {
|
await Command.checkLoggedInIf(!!options.fleet);
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
|
||||||
}
|
|
||||||
options.application ||= options.fleet;
|
|
||||||
|
|
||||||
await Command.checkLoggedInIf(!!options.application);
|
|
||||||
|
|
||||||
(await import('events')).defaultMaxListeners = 1000;
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
@ -161,19 +145,17 @@ ${dockerignoreHelp}
|
|||||||
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
protected async validateOptions(opts: FlagsDef, sdk: BalenaSDK) {
|
||||||
// Validate option combinations
|
// Validate option combinations
|
||||||
if (
|
if (
|
||||||
(opts.application == null &&
|
(opts.fleet == null && (opts.arch == null || opts.deviceType == null)) ||
|
||||||
(opts.arch == null || opts.deviceType == null)) ||
|
(opts.fleet != null && (opts.arch != null || opts.deviceType != null))
|
||||||
(opts.application != null &&
|
|
||||||
(opts.arch != null || opts.deviceType != null))
|
|
||||||
) {
|
) {
|
||||||
const { ExpectedError } = await import('../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
'You must specify either a fleet (-f), or the device type (-d) and architecture (-A)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate project directory
|
// Validate project directory
|
||||||
const { validateProjectDirectory } = await import('../utils/compose_ts');
|
const { validateProjectDirectory } = await import('../../utils/compose_ts');
|
||||||
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
const { dockerfilePath, registrySecrets } = await validateProjectDirectory(
|
||||||
sdk,
|
sdk,
|
||||||
{
|
{
|
||||||
@ -189,9 +171,9 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async getAppAndResolveArch(opts: FlagsDef) {
|
protected async getAppAndResolveArch(opts: FlagsDef) {
|
||||||
if (opts.application) {
|
if (opts.fleet) {
|
||||||
const { getAppWithArch } = await import('../utils/helpers');
|
const { getAppWithArch } = await import('../../utils/helpers');
|
||||||
const app = await getAppWithArch(opts.application);
|
const app = await getAppWithArch(opts.fleet);
|
||||||
opts.arch = app.arch;
|
opts.arch = app.arch;
|
||||||
opts.deviceType = app.is_for__device_type[0].slug;
|
opts.deviceType = app.is_for__device_type[0].slug;
|
||||||
return app;
|
return app;
|
||||||
@ -199,7 +181,7 @@ ${dockerignoreHelp}
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async prepareBuild(options: FlagsDef) {
|
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([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
getDocker(options),
|
getDocker(options),
|
||||||
generateBuildOpts(options),
|
generateBuildOpts(options),
|
||||||
@ -220,24 +202,26 @@ ${dockerignoreHelp}
|
|||||||
* buildEmulated
|
* buildEmulated
|
||||||
* buildOpts: arguments to forward to docker build command
|
* buildOpts: arguments to forward to docker build command
|
||||||
*
|
*
|
||||||
* @param {DockerToolbelt} docker
|
* @param {Dockerode} docker
|
||||||
* @param {Logger} logger
|
* @param {Logger} logger
|
||||||
* @param {ComposeOpts} composeOpts
|
* @param {ComposeOpts} composeOpts
|
||||||
* @param opts
|
* @param opts
|
||||||
*/
|
*/
|
||||||
protected async buildProject(
|
protected async buildProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../utils/logger'),
|
logger: import('../../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
app?: Application;
|
app?: {
|
||||||
|
application_type: [Pick<ApplicationType, 'supports_multicontainer'>];
|
||||||
|
};
|
||||||
arch: string;
|
arch: string;
|
||||||
deviceType: string;
|
deviceType: string;
|
||||||
buildEmulated: boolean;
|
buildEmulated: boolean;
|
||||||
buildOpts: BuildOpts;
|
buildOpts: BuildOpts;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { loadProject } = await import('../utils/compose_ts');
|
const { loadProject } = await import('../../utils/compose_ts');
|
||||||
|
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
logger,
|
logger,
|
||||||
@ -246,7 +230,7 @@ ${dockerignoreHelp}
|
|||||||
opts.buildOpts.t,
|
opts.buildOpts.t,
|
||||||
);
|
);
|
||||||
|
|
||||||
const appType = (opts.app?.application_type as ApplicationType[])?.[0];
|
const appType = opts.app?.application_type?.[0];
|
||||||
if (
|
if (
|
||||||
appType != null &&
|
appType != null &&
|
||||||
project.descriptors.length > 1 &&
|
project.descriptors.length > 1 &&
|
||||||
@ -271,7 +255,6 @@ ${dockerignoreHelp}
|
|||||||
inlineLogs: composeOpts.inlineLogs,
|
inlineLogs: composeOpts.inlineLogs,
|
||||||
convertEol: composeOpts.convertEol,
|
convertEol: composeOpts.convertEol,
|
||||||
dockerfilePath: composeOpts.dockerfilePath,
|
dockerfilePath: composeOpts.dockerfilePath,
|
||||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
|
||||||
multiDockerignore: composeOpts.multiDockerignore,
|
multiDockerignore: composeOpts.multiDockerignore,
|
||||||
});
|
});
|
||||||
}
|
}
|
@ -15,35 +15,17 @@
|
|||||||
* limitations under the License.
|
* 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 Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliForm, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import {
|
||||||
applicationIdInfo,
|
applicationIdInfo,
|
||||||
appToFleetFlagMsg,
|
devModeInfo,
|
||||||
warnify,
|
secureBootInfo,
|
||||||
} from '../../utils/messages';
|
} from '../../utils/messages';
|
||||||
import { isV13 } from '../../utils/version';
|
import type { BalenaSDK, PineDeferred } from 'balena-sdk';
|
||||||
import type { PineDeferred } from 'balena-sdk';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
version: string; // OS version
|
|
||||||
application?: string;
|
|
||||||
app?: string; // application alias
|
|
||||||
fleet?: string;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigGenerateCmd extends Command {
|
export default class ConfigGenerateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -53,6 +35,10 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
|
|
||||||
The target balenaOS version must be specified with the --version option.
|
The target balenaOS version must be specified with the --version option.
|
||||||
|
|
||||||
|
${devModeInfo.split('\n').join('\n\t\t')}
|
||||||
|
|
||||||
|
${secureBootInfo.split('\n').join('\n\t\t')}
|
||||||
|
|
||||||
To configure an image for a fleet of mixed device types, use the --fleet option
|
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.
|
alongside the --deviceType option to specify the target device type.
|
||||||
|
|
||||||
@ -65,94 +51,106 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
'$ 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 --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 --device 7cf02a6 --version 2.12.7 --output config.json',
|
||||||
'$ balena config generate --fleet MyFleet --version 2.12.7',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --dev',
|
||||||
'$ balena config generate --fleet myorg/myfleet --version 2.12.7',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --secureBoot',
|
||||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --deviceType fincm3',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --deviceType fincm3',
|
||||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --output config.json',
|
'$ balena config generate --fleet myorg/fleet --version 2.12.7 --output config.json',
|
||||||
'$ balena config generate --fleet MyFleet --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 15',
|
'$ 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 usage = 'config generate';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
version: flags.string({
|
version: Flags.string({
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
required: true,
|
required: true,
|
||||||
}),
|
}),
|
||||||
...(isV13()
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
? {}
|
dev: cf.dev,
|
||||||
: {
|
secureBoot: cf.secureBoot,
|
||||||
application: {
|
device: {
|
||||||
...cf.application,
|
...cf.device,
|
||||||
exclusive: ['app', 'fleet', 'device'],
|
exclusive: [
|
||||||
},
|
'fleet',
|
||||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'device'] },
|
'provisioning-key-name',
|
||||||
appUpdatePollInterval: flags.string({
|
'provisioning-key-expiry-date',
|
||||||
description: 'DEPRECATED alias for --updatePollInterval',
|
],
|
||||||
}),
|
},
|
||||||
}),
|
deviceApiKey: Flags.string({
|
||||||
fleet: { ...cf.fleet, exclusive: ['application', 'app', 'device'] },
|
|
||||||
device: { ...cf.device, exclusive: ['application', 'app', 'fleet'] },
|
|
||||||
deviceApiKey: flags.string({
|
|
||||||
description:
|
description:
|
||||||
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
||||||
char: 'k',
|
char: 'k',
|
||||||
}),
|
}),
|
||||||
deviceType: flags.string({
|
deviceType: Flags.string({
|
||||||
description:
|
description:
|
||||||
"device type slug (run 'balena devices supported' for possible values)",
|
"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',
|
description: 'generate a fresh device key for the device',
|
||||||
}),
|
}),
|
||||||
output: flags.string({
|
output: Flags.string({
|
||||||
description: 'path of output file',
|
description: 'path of output file',
|
||||||
char: 'o',
|
char: 'o',
|
||||||
}),
|
}),
|
||||||
// Options for non-interactive configuration
|
// Options for non-interactive configuration
|
||||||
network: flags.string({
|
network: Flags.string({
|
||||||
description: 'the network type to use: ethernet or wifi',
|
description: 'the network type to use: ethernet or wifi',
|
||||||
options: ['ethernet', 'wifi'],
|
options: ['ethernet', 'wifi'],
|
||||||
}),
|
}),
|
||||||
wifiSsid: flags.string({
|
wifiSsid: Flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi ssid to use (used only if --network is set to wifi)',
|
'the wifi ssid to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
wifiKey: flags.string({
|
wifiKey: Flags.string({
|
||||||
description:
|
description:
|
||||||
'the wifi key to use (used only if --network is set to wifi)',
|
'the wifi key to use (used only if --network is set to wifi)',
|
||||||
}),
|
}),
|
||||||
appUpdatePollInterval: flags.string({
|
appUpdatePollInterval: Flags.string({
|
||||||
description:
|
description:
|
||||||
'supervisor cloud polling interval in minutes (e.g. for variable 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,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async getApplication(balena: BalenaSDK, fleet: string) {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
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();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
await this.validateOptions(options);
|
await this.validateOptions(options);
|
||||||
|
|
||||||
let resourceDeviceType: string;
|
let resourceDeviceType: string;
|
||||||
let application: ApplicationWithDeviceType | null = null;
|
let application: Awaited<ReturnType<typeof this.getApplication>> | null =
|
||||||
|
null;
|
||||||
let device:
|
let device:
|
||||||
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
||||||
| null = null;
|
| null = null;
|
||||||
if (options.device != null) {
|
if (options.device != null) {
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
const rawDevice = await balena.models.device.get(options.device, {
|
||||||
const rawDevice = await balena.models.device.get(
|
$expand: { is_of__device_type: { $select: 'slug' } },
|
||||||
tryAsInteger(options.device),
|
});
|
||||||
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
|
||||||
);
|
|
||||||
if (!rawDevice.belongs_to__application) {
|
if (!rawDevice.belongs_to__application) {
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
throw new ExpectedError(stripIndent`
|
throw new ExpectedError(stripIndent`
|
||||||
@ -165,43 +163,51 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
resourceDeviceType = device.is_of__device_type[0].slug;
|
resourceDeviceType = device.is_of__device_type[0].slug;
|
||||||
} else {
|
} else {
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
||||||
application = (await getApplication(balena, options.application!, {
|
application = await this.getApplication(balena, options.fleet!);
|
||||||
$expand: {
|
|
||||||
is_for__device_type: { $select: 'slug' },
|
|
||||||
},
|
|
||||||
})) as ApplicationWithDeviceType;
|
|
||||||
resourceDeviceType = application.is_for__device_type[0].slug;
|
resourceDeviceType = application.is_for__device_type[0].slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceType = options.deviceType || resourceDeviceType;
|
const deviceType = options.deviceType || resourceDeviceType;
|
||||||
|
|
||||||
const deviceManifest = await balena.models.device.getManifestBySlug(
|
|
||||||
deviceType,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check compatibility if application and deviceType provided
|
// Check compatibility if application and deviceType provided
|
||||||
if (options.application && options.deviceType) {
|
if (options.fleet && options.deviceType) {
|
||||||
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
|
||||||
resourceDeviceType,
|
|
||||||
);
|
|
||||||
|
|
||||||
const helpers = await import('../../utils/helpers');
|
const helpers = await import('../../utils/helpers');
|
||||||
if (
|
if (
|
||||||
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
!(await helpers.areDeviceTypesCompatible(
|
||||||
|
resourceDeviceType,
|
||||||
|
deviceType,
|
||||||
|
))
|
||||||
) {
|
) {
|
||||||
throw new balena.errors.BalenaInvalidDeviceType(
|
const { ExpectedError } = await import('../../errors');
|
||||||
`Device type ${options.deviceType} is incompatible with fleet ${options.application}`,
|
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
|
// Prompt for values
|
||||||
// Pass params as an override: if there is any param with exactly the same name as a
|
// 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)
|
// required option, that value is used (and the corresponding question is not asked)
|
||||||
const answers = await getCliForm().run(deviceManifest.options, {
|
const answers = await getCliForm().run(deviceManifest.options, {
|
||||||
override: options,
|
override: { ...options, app: options.fleet, application: options.fleet },
|
||||||
});
|
});
|
||||||
answers.version = options.version;
|
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
|
// Generate config
|
||||||
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
||||||
@ -241,22 +247,21 @@ export default class ConfigGenerateCmd extends Command {
|
|||||||
protected readonly deviceTypeNotAllowedMessage =
|
protected readonly deviceTypeNotAllowedMessage =
|
||||||
'The --deviceType option can only be used alongside the --fleet option';
|
'The --deviceType option can only be used alongside the --fleet option';
|
||||||
|
|
||||||
protected async validateOptions(options: FlagsDef) {
|
protected async validateOptions(
|
||||||
|
options: Interfaces.InferredFlags<typeof ConfigGenerateCmd.flags>,
|
||||||
|
) {
|
||||||
const { ExpectedError } = await import('../../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
|
|
||||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
if (options.device == null && options.fleet == null) {
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
|
||||||
}
|
|
||||||
options.application ||= options.app || options.fleet;
|
|
||||||
// Prefer options.application over options.app
|
|
||||||
delete options.app;
|
|
||||||
|
|
||||||
if (options.device == null && options.application == null) {
|
|
||||||
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.application && options.deviceType) {
|
if (!options.fleet && options.deviceType) {
|
||||||
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,59 +15,46 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type: string;
|
|
||||||
drive?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
file: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigInjectCmd extends Command {
|
export default class ConfigInjectCmd extends Command {
|
||||||
public static description = stripIndent`
|
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 a mounted filesystem, e.g. the SD card of a
|
Inject a 'config.json' file to a balenaOS image file or attached SD card or
|
||||||
provisioned device or balenaOS image.
|
USB stick.
|
||||||
|
|
||||||
Note: if using a private/custom device type, please ensure you are logged in
|
Documentation for the balenaOS 'config.json' file can be found at:
|
||||||
('balena login' command). Public device types do not require logging in.
|
https://www.balena.io/docs/reference/OS/configuration/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena config inject my/config.json --type raspberrypi3',
|
'$ balena config inject my/config.json',
|
||||||
'$ balena config inject my/config.json --type raspberrypi3 --drive /dev/disk2',
|
'$ balena config inject my/config.json --drive /dev/disk2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
file: Args.string({
|
||||||
name: 'file',
|
|
||||||
description: 'the path to the config.json file to inject',
|
description: 'the path to the config.json file to inject',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'config inject <file>';
|
public static usage = 'config inject <file>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
type: cf.deviceType,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(ConfigInjectCmd);
|
||||||
ConfigInjectCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -81,7 +68,7 @@ export default class ConfigInjectCmd extends Command {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
await config.write(drive, options.type, configJSON);
|
await config.write(drive, '', configJSON);
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
}
|
}
|
||||||
|
@ -15,44 +15,40 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type: string;
|
|
||||||
drive?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigReadCmd extends Command {
|
export default class ConfigReadCmd extends Command {
|
||||||
public static description = stripIndent`
|
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,
|
Read the 'config.json' file of a balenaOS image file or attached SD card or
|
||||||
e.g. the SD card of a provisioned device or balenaOS image.
|
USB stick.
|
||||||
|
|
||||||
|
Documentation for the balenaOS 'config.json' file can be found at:
|
||||||
|
https://www.balena.io/docs/reference/OS/configuration/
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena config read --type raspberrypi3',
|
'$ balena config read',
|
||||||
'$ balena config read --type raspberrypi3 --drive /dev/disk2',
|
'$ balena config read --drive /dev/disk2',
|
||||||
|
'$ balena config read --drive balena.img',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'config read';
|
public static usage = 'config read';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
type: cf.deviceType,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
|
json: cf.json,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReadCmd);
|
const { flags: options } = await this.parse(ConfigReadCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -61,9 +57,13 @@ export default class ConfigReadCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
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');
|
if (options.json) {
|
||||||
console.info(prettyjson.render(configJSON));
|
console.log(JSON.stringify(configJSON, null, 4));
|
||||||
|
} else {
|
||||||
|
const prettyjson = await import('prettyjson');
|
||||||
|
console.log(prettyjson.render(configJSON));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,48 +15,49 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
import { getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
type: string;
|
|
||||||
drive?: string;
|
|
||||||
advanced: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ConfigReconfigureCmd extends Command {
|
export default class ConfigReconfigureCmd extends Command {
|
||||||
public static description = stripIndent`
|
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 = [
|
public static examples = [
|
||||||
'$ balena config reconfigure --type raspberrypi3',
|
'$ balena config reconfigure',
|
||||||
'$ balena config reconfigure --type raspberrypi3 --advanced',
|
'$ balena config reconfigure --drive /dev/disk3',
|
||||||
'$ balena config reconfigure --type raspberrypi3 --drive /dev/disk2',
|
'$ balena config reconfigure --drive balena.img --advanced',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = 'config reconfigure';
|
public static usage = 'config reconfigure';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
type: cf.deviceType,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
advanced: flags.boolean({
|
advanced: Flags.boolean({
|
||||||
description: 'show advanced commands',
|
description: 'show advanced commands',
|
||||||
char: 'v',
|
char: 'v',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
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 authenticated = true;
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(ConfigReconfigureCmd);
|
const { flags: options } = await this.parse(ConfigReconfigureCmd);
|
||||||
|
|
||||||
const { safeUmount } = await import('../../utils/umount');
|
const { safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -65,10 +66,20 @@ export default class ConfigReconfigureCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
const config = await import('balena-config-json');
|
||||||
const { uuid } = await config.read(drive, options.type);
|
const { uuid } = await config.read(drive, '');
|
||||||
await safeUmount(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];
|
const configureCommand = ['os', 'configure', drive, '--device', uuid];
|
||||||
|
if (options.version) {
|
||||||
|
configureCommand.push('--version', options.version);
|
||||||
|
}
|
||||||
if (options.advanced) {
|
if (options.advanced) {
|
||||||
configureCommand.push('--advanced');
|
configureCommand.push('--advanced');
|
||||||
}
|
}
|
||||||
|
@ -15,65 +15,51 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getVisuals, stripIndent } from '../../utils/lazy';
|
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 {
|
export default class ConfigWriteCmd extends Command {
|
||||||
public static description = stripIndent`
|
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,
|
Write a key-value pair to the 'config.json' file of a balenaOS image file or
|
||||||
e.g. the SD card of a provisioned device or balenaOS image.
|
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 = [
|
public static examples = [
|
||||||
'$ balena config write --type raspberrypi3 username johndoe',
|
'$ balena config write ntpServers "0.resinio.pool.ntp.org 1.resinio.pool.ntp.org"',
|
||||||
'$ balena config write --type raspberrypi3 --drive /dev/disk2 username johndoe',
|
'$ balena config write --drive /dev/disk2 hostname custom-hostname',
|
||||||
'$ balena config write --type raspberrypi3 files.network/settings "..."',
|
'$ balena config write --drive balena.img os.network.connectivity.interval 300',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
key: Args.string({
|
||||||
name: 'key',
|
|
||||||
description: 'the key of the config parameter to write',
|
description: 'the key of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
description: 'the value of the config parameter to write',
|
description: 'the value of the config parameter to write',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'config write <key> <value>';
|
public static usage = 'config write <key> <value>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
type: cf.deviceType,
|
|
||||||
drive: cf.driveOrImg,
|
drive: cf.driveOrImg,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(ConfigWriteCmd);
|
||||||
ConfigWriteCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||||
|
|
||||||
@ -82,14 +68,14 @@ export default class ConfigWriteCmd extends Command {
|
|||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
|
|
||||||
const config = await import('balena-config-json');
|
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}`);
|
console.info(`Setting ${params.key} to ${params.value}`);
|
||||||
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
ConfigWriteCmd.updateConfigJson(configJSON, params.key, params.value);
|
||||||
|
|
||||||
await denyMount(drive, async () => {
|
await denyMount(drive, async () => {
|
||||||
await safeUmount(drive);
|
await safeUmount(drive);
|
||||||
await config.write(drive, options.type, configJSON);
|
await config.write(drive, '', configJSON);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.info('Done');
|
console.info('Done');
|
||||||
|
@ -15,59 +15,56 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args, Flags } from '@oclif/core';
|
||||||
import type { ImageDescriptor } from 'resin-compose-parse';
|
import type { ImageDescriptor } from '@balena/compose/dist/parse';
|
||||||
|
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, getChalk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, getChalk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import {
|
||||||
dockerignoreHelp,
|
dockerignoreHelp,
|
||||||
registrySecretsHelp,
|
registrySecretsHelp,
|
||||||
buildArgDeprecation,
|
buildArgDeprecation,
|
||||||
} from '../utils/messages';
|
} from '../../utils/messages';
|
||||||
import * as ca from '../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import * as compose from '../utils/compose';
|
import * as compose from '../../utils/compose';
|
||||||
import type {
|
import type {
|
||||||
BuiltImage,
|
BuiltImage,
|
||||||
ComposeCliFlags,
|
ComposeCliFlags,
|
||||||
ComposeOpts,
|
ComposeOpts,
|
||||||
Release as ComposeReleaseInfo,
|
Release as ComposeReleaseInfo,
|
||||||
} from '../utils/compose-types';
|
} from '../../utils/compose-types';
|
||||||
import type { BuildOpts, DockerCliFlags } from '../utils/docker';
|
import type { BuildOpts, DockerCliFlags } from '../../utils/docker';
|
||||||
import {
|
import {
|
||||||
applyReleaseTagKeysAndValues,
|
applyReleaseTagKeysAndValues,
|
||||||
buildProject,
|
buildProject,
|
||||||
composeCliFlags,
|
composeCliFlags,
|
||||||
isBuildConfig,
|
isBuildConfig,
|
||||||
parseReleaseTagKeysAndValues,
|
parseReleaseTagKeysAndValues,
|
||||||
} from '../utils/compose_ts';
|
} from '../../utils/compose_ts';
|
||||||
import { dockerCliFlags } from '../utils/docker';
|
import { dockerCliFlags } from '../../utils/docker';
|
||||||
import type {
|
import type { ApplicationType, DeviceType, Release } from 'balena-sdk';
|
||||||
Application,
|
|
||||||
ApplicationType,
|
|
||||||
DeviceType,
|
|
||||||
Release,
|
|
||||||
} from 'balena-sdk';
|
|
||||||
|
|
||||||
interface ApplicationWithArch extends Application {
|
interface ApplicationWithArch {
|
||||||
|
id: number;
|
||||||
arch: string;
|
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 {
|
interface FlagsDef extends ComposeCliFlags, DockerCliFlags {
|
||||||
source?: string;
|
source?: string;
|
||||||
build: boolean;
|
build: boolean;
|
||||||
nologupload: boolean;
|
nologupload: boolean;
|
||||||
'release-tag'?: string[];
|
'release-tag'?: string[];
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
|
note?: string;
|
||||||
help: void;
|
help: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
image?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeployCmd extends Command {
|
export default class DeployCmd extends Command {
|
||||||
public static description = `\
|
public static description = `\
|
||||||
Deploy a single image or a multicontainer project to a balena fleet.
|
Deploy a single image or a multicontainer project to a balena fleet.
|
||||||
@ -101,35 +98,33 @@ ${dockerignoreHelp}
|
|||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena deploy myFleet',
|
'$ balena deploy myFleet',
|
||||||
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
'$ balena deploy myorg/myfleet --build --source myBuildDir/',
|
||||||
|
'$ balena deploy myorg/myfleet --build --source myBuildDir/ --note "this is the note for this release"',
|
||||||
'$ balena deploy myorg/myfleet myRepo/myImage',
|
'$ balena deploy myorg/myfleet myRepo/myImage',
|
||||||
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
'$ balena deploy myFleet myRepo/myImage --release-tag key1 "" key2 "value2 with spaces"',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
ca.fleetRequired,
|
fleet: ca.fleetRequired,
|
||||||
{
|
image: Args.string({ description: 'the image to deploy' }),
|
||||||
name: 'image',
|
};
|
||||||
description: 'the image to deploy',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
public static usage = 'deploy <fleet> [image]';
|
public static usage = 'deploy <fleet> [image]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
source: flags.string({
|
source: Flags.string({
|
||||||
description:
|
description:
|
||||||
'specify an alternate source directory; default is the working directory',
|
'specify an alternate source directory; default is the working directory',
|
||||||
char: 's',
|
char: 's',
|
||||||
}),
|
}),
|
||||||
build: flags.boolean({
|
build: Flags.boolean({
|
||||||
description: 'force a rebuild before deploy',
|
description: 'force a rebuild before deploy',
|
||||||
char: 'b',
|
char: 'b',
|
||||||
}),
|
}),
|
||||||
nologupload: flags.boolean({
|
nologupload: Flags.boolean({
|
||||||
description:
|
description:
|
||||||
"don't upload build logs to the dashboard with image (if building)",
|
"don't upload build logs to the dashboard with image (if building)",
|
||||||
}),
|
}),
|
||||||
'release-tag': flags.string({
|
'release-tag': Flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Set release tags if the image deployment is successful. Multiple
|
Set release tags if the image deployment is successful. Multiple
|
||||||
arguments may be provided, alternating tag keys and values (see examples).
|
arguments may be provided, alternating tag keys and values (see examples).
|
||||||
@ -137,7 +132,7 @@ ${dockerignoreHelp}
|
|||||||
`,
|
`,
|
||||||
multiple: true,
|
multiple: true,
|
||||||
}),
|
}),
|
||||||
draft: flags.boolean({
|
draft: Flags.boolean({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Deploy the release as a draft. Draft releases are ignored
|
Deploy the release as a draft. Draft releases are ignored
|
||||||
by the 'track latest' release policy but can be used through release pinning.
|
by the 'track latest' release policy but can be used through release pinning.
|
||||||
@ -145,11 +140,12 @@ ${dockerignoreHelp}
|
|||||||
as final by default unless this option is given.`,
|
as final by default unless this option is given.`,
|
||||||
default: false,
|
default: false,
|
||||||
}),
|
}),
|
||||||
|
note: Flags.string({ description: 'The notes for this release' }),
|
||||||
...composeCliFlags,
|
...composeCliFlags,
|
||||||
...dockerCliFlags,
|
...dockerCliFlags,
|
||||||
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
// NOTE: Not supporting -h for help, because of clash with -h in DockerCliFlags
|
||||||
// Revisit this in future release.
|
// Revisit this in future release.
|
||||||
help: flags.help({}),
|
help: Flags.help({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
@ -157,9 +153,7 @@ ${dockerignoreHelp}
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeployCmd);
|
||||||
DeployCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
(await import('events')).defaultMaxListeners = 1000;
|
(await import('events')).defaultMaxListeners = 1000;
|
||||||
|
|
||||||
@ -181,7 +175,7 @@ ${dockerignoreHelp}
|
|||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
const { getRegistrySecrets, validateProjectDirectory } = await import(
|
||||||
'../utils/compose_ts'
|
'../../utils/compose_ts'
|
||||||
);
|
);
|
||||||
|
|
||||||
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
const { releaseTagKeys, releaseTagValues } = parseReleaseTagKeysAndValues(
|
||||||
@ -189,7 +183,7 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
options['registry-secrets'] = await getRegistrySecrets(
|
(options as FlagsDef)['registry-secrets'] = await getRegistrySecrets(
|
||||||
sdk,
|
sdk,
|
||||||
options['registry-secrets'],
|
options['registry-secrets'],
|
||||||
);
|
);
|
||||||
@ -202,16 +196,16 @@ ${dockerignoreHelp}
|
|||||||
registrySecretsPath: options['registry-secrets'],
|
registrySecretsPath: options['registry-secrets'],
|
||||||
});
|
});
|
||||||
options.dockerfile = dockerfilePath;
|
options.dockerfile = dockerfilePath;
|
||||||
options['registry-secrets'] = registrySecrets;
|
(options as FlagsDef)['registry-secrets'] = registrySecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
const helpers = await import('../utils/helpers');
|
const helpers = await import('../../utils/helpers');
|
||||||
const app = await helpers.getAppWithArch(fleet);
|
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([
|
const [docker, buildOpts, composeOpts] = await Promise.all([
|
||||||
dockerUtils.getDocker(options),
|
dockerUtils.getDocker(options),
|
||||||
dockerUtils.generateBuildOpts(options),
|
dockerUtils.generateBuildOpts(options as FlagsDef),
|
||||||
compose.generateOpts(options),
|
compose.generateOpts(options),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -231,11 +225,14 @@ ${dockerignoreHelp}
|
|||||||
releaseTagKeys,
|
releaseTagKeys,
|
||||||
releaseTagValues,
|
releaseTagValues,
|
||||||
);
|
);
|
||||||
|
if (options.note) {
|
||||||
|
await sdk.models.release.setNote(release.id, options.note);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deployProject(
|
async deployProject(
|
||||||
docker: import('dockerode'),
|
docker: import('dockerode'),
|
||||||
logger: import('../utils/logger'),
|
logger: import('../../utils/logger'),
|
||||||
composeOpts: ComposeOpts,
|
composeOpts: ComposeOpts,
|
||||||
opts: {
|
opts: {
|
||||||
app: ApplicationWithArch; // the application instance to deploy to
|
app: ApplicationWithArch; // the application instance to deploy to
|
||||||
@ -253,10 +250,10 @@ ${dockerignoreHelp}
|
|||||||
const doodles = await import('resin-doodles');
|
const doodles = await import('resin-doodles');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const { deployProject: $deployProject, loadProject } = await import(
|
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 {
|
try {
|
||||||
const project = await loadProject(
|
const project = await loadProject(
|
||||||
@ -313,13 +310,12 @@ ${dockerignoreHelp}
|
|||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
composition: compositionToBuild,
|
composition: compositionToBuild,
|
||||||
arch: opts.app.arch,
|
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,
|
emulated: opts.buildEmulated,
|
||||||
buildOpts: opts.buildOpts,
|
buildOpts: opts.buildOpts,
|
||||||
inlineLogs: composeOpts.inlineLogs,
|
inlineLogs: composeOpts.inlineLogs,
|
||||||
convertEol: composeOpts.convertEol,
|
convertEol: composeOpts.convertEol,
|
||||||
dockerfilePath: composeOpts.dockerfilePath,
|
dockerfilePath: composeOpts.dockerfilePath,
|
||||||
nogitignore: composeOpts.nogitignore, // v13: delete this line
|
|
||||||
multiDockerignore: composeOpts.multiDockerignore,
|
multiDockerignore: composeOpts.multiDockerignore,
|
||||||
});
|
});
|
||||||
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
builtImagesByService = _.keyBy(builtImages, 'serviceName');
|
||||||
@ -335,17 +331,17 @@ ${dockerignoreHelp}
|
|||||||
);
|
);
|
||||||
|
|
||||||
let release: Release | ComposeReleaseInfo['release'];
|
let release: Release | ComposeReleaseInfo['release'];
|
||||||
if (appType?.is_legacy) {
|
if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') {
|
||||||
const { deployLegacy } = require('../utils/deploy-legacy');
|
const { deployLegacy } = require('../../utils/deploy-legacy');
|
||||||
|
|
||||||
const msg = getChalk().yellow(
|
const msg = getChalk().yellow(
|
||||||
'Target fleet requires legacy deploy method.',
|
'Target fleet requires legacy deploy method.',
|
||||||
);
|
);
|
||||||
logger.logWarn(msg);
|
logger.logWarn(msg);
|
||||||
|
|
||||||
const [token, username, url, options] = await Promise.all([
|
const [token, { username }, url, options] = await Promise.all([
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
sdk.auth.whoami(),
|
sdk.auth.getUserInfo(),
|
||||||
sdk.settings.get('balenaUrl'),
|
sdk.settings.get('balenaUrl'),
|
||||||
{
|
{
|
||||||
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name
|
||||||
@ -368,8 +364,8 @@ ${dockerignoreHelp}
|
|||||||
$select: ['commit'],
|
$select: ['commit'],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const [userId, auth, apiEndpoint] = await Promise.all([
|
const [{ id: userId }, auth, apiEndpoint] = await Promise.all([
|
||||||
sdk.auth.getUserId(),
|
sdk.auth.getUserInfo(),
|
||||||
sdk.auth.getToken(),
|
sdk.auth.getToken(),
|
||||||
sdk.settings.get('apiUrl'),
|
sdk.settings.get('apiUrl'),
|
||||||
]);
|
]);
|
@ -15,21 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceDeactivateCmd extends Command {
|
export default class DeviceDeactivateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Deactivate a device.
|
Deactivate a device.
|
||||||
@ -44,17 +34,16 @@ export default class DeviceDeactivateCmd extends Command {
|
|||||||
'$ balena device deactivate 7cf02a6 --yes',
|
'$ balena device deactivate 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the UUID of the device to be deactivated',
|
description: 'the UUID of the device to be deactivated',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device deactivate <uuid>';
|
public static usage = 'device deactivate <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -62,9 +51,8 @@ export default class DeviceDeactivateCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceDeactivateCmd,
|
await this.parse(DeviceDeactivateCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
|
@ -15,22 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceIdentifyCmd extends Command {
|
export default class DeviceIdentifyCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Identify a device.
|
Identify a device.
|
||||||
@ -39,25 +29,23 @@ export default class DeviceIdentifyCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device identify 23c73a1'];
|
public static examples = ['$ balena device identify 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to identify',
|
description: 'the uuid of the device to identify',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device identify <uuid>';
|
public static usage = 'device identify <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceIdentifyCmd);
|
const { args: params } = await this.parse(DeviceIdentifyCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
@ -15,21 +15,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { appToFleetOutputMsg, warnify } from '../../utils/messages';
|
import { jsonInfo } from '../../utils/messages';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
import type { Application, Release } from 'balena-sdk';
|
import type { Application, Release } from 'balena-sdk';
|
||||||
|
|
||||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
interface ExtendedDevice extends DeviceWithDeviceType {
|
||||||
dashboard_url?: string;
|
dashboard_url?: string;
|
||||||
application_name?: string;
|
fleet: string; // 'org/name' slug
|
||||||
device_type?: string;
|
device_type?: string;
|
||||||
commit?: string;
|
commit?: string;
|
||||||
last_seen?: string;
|
last_seen?: string;
|
||||||
@ -44,85 +41,101 @@ interface ExtendedDevice extends DeviceWithDeviceType {
|
|||||||
undervoltage_detected?: boolean;
|
undervoltage_detected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
v13: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceCmd extends Command {
|
export default class DeviceCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Show info about a single device.
|
Show info about a single device.
|
||||||
|
|
||||||
Show information about a single device.
|
Show information about a single device.
|
||||||
`;
|
|
||||||
public static examples = ['$ balena device 7cf02a6'];
|
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
${jsonInfo.split('\n').join('\n\t\t')}
|
||||||
{
|
`;
|
||||||
name: 'uuid',
|
public static examples = [
|
||||||
description: 'the device uuid',
|
'$ balena device 7cf02a6',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
'$ balena device 7cf02a6 --view',
|
||||||
required: true,
|
'$ balena device 7cf02a6 --json',
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
uuid: Args.string({
|
||||||
|
description: 'the device uuid',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
public static usage = 'device <uuid>';
|
public static usage = 'device <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
|
json: cf.json,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
v13: cf.v13,
|
view: Flags.boolean({
|
||||||
|
default: false,
|
||||||
|
description: 'open device dashboard page',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceCmd);
|
||||||
DeviceCmd,
|
|
||||||
);
|
|
||||||
const useAppWord = !options.v13 && !isV13();
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const device = (await balena.models.device.get(params.uuid, {
|
const device = (await balena.models.device.get(
|
||||||
$select: [
|
params.uuid,
|
||||||
'device_name',
|
options.json
|
||||||
'id',
|
? {
|
||||||
'overall_status',
|
$expand: {
|
||||||
'is_online',
|
device_tag: {
|
||||||
'ip_address',
|
$select: ['tag_key', 'value'],
|
||||||
'mac_address',
|
},
|
||||||
'last_connectivity_event',
|
...expandForAppName.$expand,
|
||||||
'uuid',
|
},
|
||||||
'supervisor_version',
|
}
|
||||||
'is_web_accessible',
|
: {
|
||||||
'note',
|
$select: [
|
||||||
'os_version',
|
'device_name',
|
||||||
'memory_usage',
|
'id',
|
||||||
'memory_total',
|
'overall_status',
|
||||||
'public_address',
|
'is_online',
|
||||||
'storage_block_device',
|
'ip_address',
|
||||||
'storage_usage',
|
'mac_address',
|
||||||
'storage_total',
|
'last_connectivity_event',
|
||||||
'cpu_usage',
|
'uuid',
|
||||||
'cpu_temp',
|
'supervisor_version',
|
||||||
'cpu_id',
|
'is_web_accessible',
|
||||||
'is_undervolted',
|
'note',
|
||||||
],
|
'os_version',
|
||||||
...expandForAppName,
|
'memory_usage',
|
||||||
})) as ExtendedDevice;
|
'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.status = device.overall_status;
|
||||||
|
|
||||||
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
device.dashboard_url = balena.models.device.getDashboardUrl(device.uuid);
|
||||||
|
|
||||||
const belongsToApplication =
|
const belongsToApplication =
|
||||||
device.belongs_to__application as Application[];
|
device.belongs_to__application as Application[];
|
||||||
device.application_name = belongsToApplication?.[0]
|
device.fleet = belongsToApplication?.[0]
|
||||||
? belongsToApplication[0].app_name
|
? belongsToApplication[0].slug
|
||||||
: 'N/a';
|
: 'N/a';
|
||||||
|
|
||||||
device.device_type = device.is_of__device_type[0].slug;
|
device.device_type = device.is_of__device_type[0].slug;
|
||||||
@ -170,8 +183,9 @@ export default class DeviceCmd extends Command {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useAppWord && process.stderr.isTTY) {
|
if (options.json) {
|
||||||
console.error(warnify(appToFleetOutputMsg));
|
console.log(JSON.stringify(device, null, 4));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -184,7 +198,7 @@ export default class DeviceCmd extends Command {
|
|||||||
'ip_address',
|
'ip_address',
|
||||||
'public_address',
|
'public_address',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
useAppWord ? 'application_name' : 'application_name => FLEET',
|
'fleet',
|
||||||
'last_seen',
|
'last_seen',
|
||||||
'uuid',
|
'uuid',
|
||||||
'commit',
|
'commit',
|
||||||
|
@ -15,21 +15,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetFlagMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
import { runCommand } from '../../utils/helpers';
|
import { runCommand } from '../../utils/helpers';
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
application?: string;
|
|
||||||
app?: string;
|
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
yes: boolean;
|
yes: boolean;
|
||||||
advanced: boolean;
|
advanced: boolean;
|
||||||
@ -37,42 +30,60 @@ interface FlagsDef {
|
|||||||
drive?: string;
|
drive?: string;
|
||||||
config?: string;
|
config?: string;
|
||||||
help: void;
|
help: void;
|
||||||
|
'provisioning-key-name'?: string;
|
||||||
|
'provisioning-key-expiry-date'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DeviceInitCmd extends Command {
|
export default class DeviceInitCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Initialize a device with balenaOS.
|
Initialize a device with balenaOS.
|
||||||
|
|
||||||
Initialize a device by downloading the OS image of the specified fleet
|
Register a new device in the selected fleet, download the OS image for the
|
||||||
and writing it to an SD Card.
|
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:
|
||||||
|
|
||||||
If the --fleet option is omitted, it will be prompted for interactively.
|
'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')}
|
${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.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device init',
|
'$ balena device init',
|
||||||
'$ balena device init --fleet MyFleet',
|
|
||||||
'$ balena device init -f myorg/myfleet',
|
'$ 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 usage = 'device init';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13()
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
application: cf.application,
|
|
||||||
app: cf.app,
|
|
||||||
}),
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
advanced: flags.boolean({
|
advanced: Flags.boolean({
|
||||||
char: 'v',
|
char: 'v',
|
||||||
description: 'show advanced configuration options',
|
description: 'show advanced configuration options',
|
||||||
}),
|
}),
|
||||||
'os-version': flags.string({
|
'os-version': Flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
exact version number, or a valid semver range,
|
exact version number, or a valid semver range,
|
||||||
or 'latest' (includes pre-releases),
|
or 'latest' (includes pre-releases),
|
||||||
@ -82,16 +93,23 @@ export default class DeviceInitCmd extends Command {
|
|||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
drive: cf.drive,
|
drive: cf.drive,
|
||||||
config: flags.string({
|
config: Flags.string({
|
||||||
description: 'path to the config JSON file, see `balena os build-config`',
|
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,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DeviceInitCmd);
|
const { flags: options } = await this.parse(DeviceInitCmd);
|
||||||
|
|
||||||
// Imports
|
// Imports
|
||||||
const { promisify } = await import('util');
|
const { promisify } = await import('util');
|
||||||
@ -105,32 +123,21 @@ export default class DeviceInitCmd extends Command {
|
|||||||
const logger = await Command.getLogger();
|
const logger = await Command.getLogger();
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
|
||||||
}
|
|
||||||
// Consolidate application options
|
|
||||||
options.application ||= options.app || options.fleet;
|
|
||||||
delete options.app;
|
|
||||||
|
|
||||||
// Get application and
|
// Get application and
|
||||||
const application = (await getApplication(
|
const application = options.fleet
|
||||||
balena,
|
? await getApplication(balena, options.fleet, {
|
||||||
options['application'] ||
|
$select: ['id', 'slug'],
|
||||||
(
|
$expand: {
|
||||||
await (await import('../../utils/patterns')).selectApplication()
|
is_for__device_type: {
|
||||||
).id,
|
$select: 'slug',
|
||||||
{
|
},
|
||||||
$expand: {
|
|
||||||
is_for__device_type: {
|
|
||||||
$select: 'slug',
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
: await (await import('../../utils/patterns')).selectApplication();
|
||||||
)) as ApplicationWithDeviceType;
|
|
||||||
|
|
||||||
// Register new device
|
// Register new device
|
||||||
const deviceUuid = balena.models.device.generateUniqueKey();
|
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);
|
await balena.models.device.register(application.id, deviceUuid);
|
||||||
const device = await balena.models.device.get(deviceUuid);
|
const device = await balena.models.device.get(deviceUuid);
|
||||||
|
|
||||||
@ -173,6 +180,21 @@ export default class DeviceInitCmd extends Command {
|
|||||||
} else if (options.advanced) {
|
} else if (options.advanced) {
|
||||||
configureCommand.push('--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);
|
await runCommand(configureCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,23 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
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 | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceLocalModeCmd extends Command {
|
export default class DeviceLocalModeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -48,27 +35,25 @@ export default class DeviceLocalModeCmd extends Command {
|
|||||||
'$ balena device local-mode 23c73a1 --status',
|
'$ balena device local-mode 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device local-mode <uuid>';
|
public static usage = 'device local-mode <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
enable: flags.boolean({
|
enable: Flags.boolean({
|
||||||
description: 'enable local mode',
|
description: 'enable local mode',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: flags.boolean({
|
disable: Flags.boolean({
|
||||||
description: 'disable local mode',
|
description: 'disable local mode',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: flags.boolean({
|
status: Flags.boolean({
|
||||||
description: 'output boolean indicating local mode status',
|
description: 'output boolean indicating local mode status',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
@ -78,9 +63,8 @@ export default class DeviceLocalModeCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceLocalModeCmd,
|
await this.parse(DeviceLocalModeCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
@ -15,42 +15,18 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import type {
|
import type {
|
||||||
BalenaSDK,
|
BalenaSDK,
|
||||||
Device,
|
Device,
|
||||||
DeviceType,
|
PineOptions,
|
||||||
PineTypedResult,
|
PineTypedResult,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetFlagMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
type ExtendedDevice = PineTypedResult<
|
|
||||||
Device,
|
|
||||||
typeof import('../../utils/helpers').expandForAppNameAndCpuArch
|
|
||||||
> & {
|
|
||||||
application_name?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
application?: string;
|
|
||||||
app?: string;
|
|
||||||
fleet?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceMoveCmd extends Command {
|
export default class DeviceMoveCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -70,74 +46,70 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
'$ balena device move 7cf02a6 -f myorg/mynewfleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
'comma-separated list (no blank spaces) of device UUIDs to be moved',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device move <uuid(s)>';
|
public static usage = 'device move <uuid(s)>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13() ? {} : { app: cf.app, application: cf.application }),
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
private async getDevices(balena: BalenaSDK, deviceUuids: string[]) {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const deviceOptions = {
|
||||||
DeviceMoveCmd,
|
$select: 'belongs_to__application',
|
||||||
);
|
$expand: {
|
||||||
|
is_of__device_type: {
|
||||||
|
$select: 'is_of__cpu_architecture',
|
||||||
|
$expand: {
|
||||||
|
is_of__cpu_architecture: {
|
||||||
|
$select: 'slug',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies PineOptions<Device>;
|
||||||
|
|
||||||
if ((options.application || options.app) && process.stderr.isTTY) {
|
// TODO: Refacor once `device.get()` accepts an array of uuids`
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
const devices = await Promise.all(
|
||||||
}
|
deviceUuids.map(
|
||||||
options.application ||= options.app || options.fleet;
|
(uuid) =>
|
||||||
|
balena.models.device.get(uuid, deviceOptions) as Promise<
|
||||||
|
PineTypedResult<Device, typeof deviceOptions>
|
||||||
|
>,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params, flags: options } = await this.parse(DeviceMoveCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
// Split uuids string into array of uuids
|
||||||
const { expandForAppNameAndCpuArch } = await import('../../utils/helpers');
|
const deviceUuids = params.uuid.split(',');
|
||||||
|
|
||||||
// Parse ids string into array of correct types
|
const devices = await this.getDevices(balena, deviceUuids);
|
||||||
const deviceIds: Array<string | number> = params.uuid
|
|
||||||
.split(',')
|
|
||||||
.map((id) => tryAsInteger(id));
|
|
||||||
|
|
||||||
// Get devices
|
// Disambiguate application
|
||||||
const devices = await Promise.all(
|
|
||||||
deviceIds.map(
|
|
||||||
(uuid) =>
|
|
||||||
balena.models.device.get(
|
|
||||||
uuid,
|
|
||||||
expandForAppNameAndCpuArch,
|
|
||||||
) as Promise<ExtendedDevice>,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Map application name for each device
|
|
||||||
for (const device of devices) {
|
|
||||||
const belongsToApplication = device.belongs_to__application;
|
|
||||||
device.application_name = belongsToApplication?.[0]
|
|
||||||
? belongsToApplication[0].app_name
|
|
||||||
: 'N/a';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
// Get destination application
|
// Get destination application
|
||||||
const application = options.application
|
const application = options.fleet
|
||||||
? await getApplication(balena, options.application)
|
? await getApplication(balena, options.fleet, { $select: ['id', 'slug'] })
|
||||||
: await this.interactivelySelectApplication(balena, devices);
|
: await this.interactivelySelectApplication(balena, devices);
|
||||||
|
|
||||||
// Move each device
|
// Move each device
|
||||||
for (const uuid of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
try {
|
try {
|
||||||
await balena.models.device.move(uuid, application.id);
|
await balena.models.device.move(uuid, application.id);
|
||||||
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
console.info(`Device ${uuid} was moved to fleet ${application.slug}`);
|
||||||
@ -150,9 +122,8 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
|
|
||||||
async interactivelySelectApplication(
|
async interactivelySelectApplication(
|
||||||
balena: BalenaSDK,
|
balena: BalenaSDK,
|
||||||
devices: ExtendedDevice[],
|
devices: Awaited<ReturnType<typeof this.getDevices>>,
|
||||||
) {
|
) {
|
||||||
const { getExpandedProp } = await import('../../utils/pine');
|
|
||||||
// deduplicate the slugs
|
// deduplicate the slugs
|
||||||
const deviceCpuArchs = Array.from(
|
const deviceCpuArchs = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
@ -162,46 +133,44 @@ export default class DeviceMoveCmd extends Command {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceTypeOptions = {
|
const allCpuArches = await balena.pine.get({
|
||||||
$select: 'slug',
|
resource: 'cpu_architecture',
|
||||||
$expand: {
|
options: {
|
||||||
is_of__cpu_architecture: {
|
$select: ['id', 'slug'],
|
||||||
$select: 'slug',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as const;
|
});
|
||||||
const deviceTypes = (await balena.models.deviceType.getAllSupported(
|
|
||||||
deviceTypeOptions,
|
|
||||||
)) as Array<PineTypedResult<DeviceType, typeof deviceTypeOptions>>;
|
|
||||||
|
|
||||||
const compatibleDeviceTypeSlugs = new Set(
|
const compatibleCpuArchIds = allCpuArches
|
||||||
deviceTypes
|
.filter((cpuArch) => {
|
||||||
.filter((deviceType) => {
|
return deviceCpuArchs.every((deviceCpuArch) =>
|
||||||
const deviceTypeArch = getExpandedProp(
|
balena.models.os.isArchitectureCompatibleWith(
|
||||||
deviceType.is_of__cpu_architecture,
|
deviceCpuArch,
|
||||||
'slug',
|
cpuArch.slug,
|
||||||
)!;
|
),
|
||||||
return deviceCpuArchs.every((deviceCpuArch) =>
|
);
|
||||||
balena.models.os.isArchitectureCompatibleWith(
|
})
|
||||||
deviceCpuArch,
|
.map((deviceType) => deviceType.id);
|
||||||
deviceTypeArch,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((deviceType) => deviceType.slug),
|
|
||||||
);
|
|
||||||
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
try {
|
try {
|
||||||
const application = await patterns.selectApplication(
|
const application = await patterns.selectApplication(
|
||||||
(app) =>
|
{
|
||||||
compatibleDeviceTypeSlugs.has(app.is_for__device_type[0].slug) &&
|
is_for__device_type: {
|
||||||
devices.some((device) => device.application_name !== app.app_name),
|
$any: {
|
||||||
|
$alias: 'dt',
|
||||||
|
$expr: {
|
||||||
|
dt: {
|
||||||
|
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
return application;
|
return application;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!compatibleDeviceTypeSlugs.size) {
|
if (!compatibleCpuArchIds.length) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`${err.message}\nDo all devices have a compatible architecture?`,
|
`${err.message}\nDo all devices have a compatible architecture?`,
|
||||||
);
|
);
|
||||||
|
@ -15,25 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import type { Device } from 'balena-sdk';
|
import type { Device } from 'balena-sdk';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
version?: string;
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceOsUpdateCmd extends Command {
|
export default class DeviceOsUpdateCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Start a Host OS update for a device.
|
Start a Host OS update for a device.
|
||||||
@ -47,22 +35,21 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena device os-update 23c73a1',
|
'$ 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 --version 2.31.0+rev1.prod',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to update',
|
description: 'the uuid of the device to update',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device os-update <uuid>';
|
public static usage = 'device os-update <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
version: flags.string({
|
version: Flags.string({
|
||||||
description: 'a balenaOS version',
|
description: 'a balenaOS version',
|
||||||
}),
|
}),
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
@ -72,9 +59,8 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceOsUpdateCmd,
|
await this.parse(DeviceOsUpdateCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
@ -114,6 +100,8 @@ export default class DeviceOsUpdateCmd extends Command {
|
|||||||
// Get target OS version
|
// Get target OS version
|
||||||
let targetOsVersion = options.version;
|
let targetOsVersion = options.version;
|
||||||
if (targetOsVersion != null) {
|
if (targetOsVersion != null) {
|
||||||
|
const { normalizeOsVersion } = await import('../../utils/normalization');
|
||||||
|
targetOsVersion = normalizeOsVersion(targetOsVersion);
|
||||||
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
if (!hupVersionInfo.versions.includes(targetOsVersion)) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`The provided version ${targetOsVersion} is not in the Host OS update targets for this device`,
|
`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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
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 {
|
export default class DevicePublicUrlCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -43,9 +28,6 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
This command will output the current public URL for the
|
This command will output the current public URL for the
|
||||||
specified device. It can also enable or disable the URL,
|
specified device. It can also enable or disable the URL,
|
||||||
or output the enabled status, using the respective options.
|
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 = [
|
public static examples = [
|
||||||
@ -55,33 +37,25 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
'$ balena device public-url 23c73a1 --status',
|
'$ balena device public-url 23c73a1 --status',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to manage',
|
description: 'the uuid of the device to manage',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
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 usage = 'device public-url <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
enable: flags.boolean({
|
enable: Flags.boolean({
|
||||||
description: 'enable the public URL',
|
description: 'enable the public URL',
|
||||||
exclusive: ['disable', 'status'],
|
exclusive: ['disable', 'status'],
|
||||||
}),
|
}),
|
||||||
disable: flags.boolean({
|
disable: Flags.boolean({
|
||||||
description: 'disable the public URL',
|
description: 'disable the public URL',
|
||||||
exclusive: ['enable', 'status'],
|
exclusive: ['enable', 'status'],
|
||||||
}),
|
}),
|
||||||
status: flags.boolean({
|
status: Flags.boolean({
|
||||||
description: 'determine if public URL is enabled',
|
description: 'determine if public URL is enabled',
|
||||||
exclusive: ['enable', 'disable'],
|
exclusive: ['enable', 'disable'],
|
||||||
}),
|
}),
|
||||||
@ -91,28 +65,8 @@ export default class DevicePublicUrlCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DevicePublicUrlCmd,
|
await this.parse(DevicePublicUrlCmd);
|
||||||
);
|
|
||||||
|
|
||||||
// Legacy command format support.
|
|
||||||
// Previously this command used the following format
|
|
||||||
// (changed due to oclif technicalities):
|
|
||||||
// `balena device public-url enable|disable|status <uuid>`
|
|
||||||
if (params.legacyUuid) {
|
|
||||||
const action = params.uuid;
|
|
||||||
if (!['enable', 'disable', 'status'].includes(action)) {
|
|
||||||
throw new ExpectedError(
|
|
||||||
`Unexpected arguments: ${params.uuid} ${params.legacyUuid}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.enable = action === 'enable';
|
|
||||||
options.disable = action === 'disable';
|
|
||||||
options.status = action === 'status';
|
|
||||||
params.uuid = params.legacyUuid;
|
|
||||||
delete params.legacyUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
@ -15,20 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicePurgeCmd extends Command {
|
export default class DevicePurgeCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Purge data from a device.
|
Purge data from a device.
|
||||||
@ -46,34 +37,30 @@ export default class DevicePurgeCmd extends Command {
|
|||||||
|
|
||||||
public static usage = 'device purge <uuid>';
|
public static usage = 'device purge <uuid>';
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
description: 'comma-separated list (no blank spaces) of device UUIDs',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
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 balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
||||||
|
|
||||||
const deviceIds = params.uuid.split(',').map((id) => {
|
const deviceUuids = params.uuid.split(',');
|
||||||
return tryAsInteger(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const deviceId of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
ux.action.start(`Purging data from device ${deviceId}`);
|
ux.action.start(`Purging data from device ${uuid}`);
|
||||||
await balena.models.device.purge(deviceId);
|
await balena.models.device.purge(uuid);
|
||||||
ux.action.stop();
|
ux.action.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
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 {
|
export default class DeviceRebootCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -39,18 +28,16 @@ export default class DeviceRebootCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device reboot 23c73a1'];
|
public static examples = ['$ balena device reboot 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to reboot',
|
description: 'the uuid of the device to reboot',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device reboot <uuid>';
|
public static usage = 'device reboot <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -58,9 +45,7 @@ export default class DeviceRebootCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRebootCmd);
|
||||||
DeviceRebootCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
@ -15,23 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as ca from '../../utils/common-args';
|
import * as ca from '../../utils/common-args';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
uuid?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
fleet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRegisterCmd extends Command {
|
export default class DeviceRegisterCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Register a new device.
|
Register a new device.
|
||||||
@ -47,37 +37,49 @@ export default class DeviceRegisterCmd extends Command {
|
|||||||
'$ balena device register MyFleet',
|
'$ balena device register MyFleet',
|
||||||
'$ balena device register MyFleet --uuid <uuid>',
|
'$ balena device register MyFleet --uuid <uuid>',
|
||||||
'$ balena device register myorg/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>> = [ca.fleetRequired];
|
public static args = {
|
||||||
|
fleet: ca.fleetRequired,
|
||||||
|
};
|
||||||
|
|
||||||
public static usage = 'device register <fleet>';
|
public static usage = 'device register <fleet>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
uuid: flags.string({
|
uuid: Flags.string({
|
||||||
description: 'custom uuid',
|
description: 'custom uuid',
|
||||||
char: 'u',
|
char: 'u',
|
||||||
}),
|
}),
|
||||||
|
deviceType: Flags.string({
|
||||||
|
description:
|
||||||
|
"device type slug (run 'balena devices supported' for possible values)",
|
||||||
|
}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceRegisterCmd,
|
await this.parse(DeviceRegisterCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
const application = await getApplication(balena, params.fleet);
|
const application = await getApplication(balena, params.fleet, {
|
||||||
|
$select: ['id', 'slug'],
|
||||||
|
});
|
||||||
const uuid = options.uuid ?? balena.models.device.generateUniqueKey();
|
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;
|
return result && result.uuid;
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
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 {
|
export default class DeviceRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -44,29 +33,26 @@ export default class DeviceRenameCmd extends Command {
|
|||||||
'$ balena device rename 7cf02a6 MyPi',
|
'$ balena device rename 7cf02a6 MyPi',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to rename',
|
description: 'the uuid of the device to rename',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
newName: Args.string({
|
||||||
name: 'newName',
|
|
||||||
description: 'the new name for the device',
|
description: 'the new name for the device',
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device rename <uuid> [newName]';
|
public static usage = 'device rename <uuid> [newName]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(DeviceRenameCmd);
|
const { args: params } = await this.parse(DeviceRenameCmd);
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
@ -15,8 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getCliUx, stripIndent } from '../../utils/lazy';
|
||||||
@ -26,15 +25,6 @@ import type {
|
|||||||
CurrentServiceWithCommit,
|
CurrentServiceWithCommit,
|
||||||
} from 'balena-sdk';
|
} from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
service?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceRestartCmd extends Command {
|
export default class DeviceRestartCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Restart containers on a device.
|
Restart containers on a device.
|
||||||
@ -55,19 +45,18 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
'$ balena device restart 23c73a1 -s myService1,myService2',
|
'$ balena device restart 23c73a1 -s myService1,myService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
'comma-separated list (no blank spaces) of device UUIDs to restart',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device restart <uuid>';
|
public static usage = 'device restart <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
service: flags.string({
|
service: Flags.string({
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of service names to restart',
|
'comma-separated list (no blank spaces) of service names to restart',
|
||||||
char: 's',
|
char: 's',
|
||||||
@ -78,28 +67,23 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRestartCmd);
|
||||||
DeviceRestartCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { tryAsInteger } = await import('../../utils/validation');
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const ux = getCliUx();
|
const ux = getCliUx();
|
||||||
|
|
||||||
const deviceIds = params.uuid.split(',').map((id) => {
|
const deviceUuids = params.uuid.split(',');
|
||||||
return tryAsInteger(id);
|
|
||||||
});
|
|
||||||
const serviceNames = options.service?.split(',');
|
const serviceNames = options.service?.split(',');
|
||||||
|
|
||||||
// Iterate sequentially through deviceIds.
|
// Iterate sequentially through deviceUuids.
|
||||||
// We may later want to add a batching feature,
|
// We may later want to add a batching feature,
|
||||||
// so that n devices are processed in parallel
|
// so that n devices are processed in parallel
|
||||||
for (const deviceId of deviceIds) {
|
for (const uuid of deviceUuids) {
|
||||||
ux.action.start(`Restarting services on device ${deviceId}`);
|
ux.action.start(`Restarting services on device ${uuid}`);
|
||||||
if (serviceNames) {
|
if (serviceNames) {
|
||||||
await this.restartServices(balena, deviceId, serviceNames);
|
await this.restartServices(balena, uuid, serviceNames);
|
||||||
} else {
|
} else {
|
||||||
await this.restartAllServices(balena, deviceId);
|
await this.restartAllServices(balena, uuid);
|
||||||
}
|
}
|
||||||
ux.action.stop();
|
ux.action.stop();
|
||||||
}
|
}
|
||||||
@ -107,7 +91,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
|
|
||||||
async restartServices(
|
async restartServices(
|
||||||
balena: BalenaSDK,
|
balena: BalenaSDK,
|
||||||
deviceId: number | string,
|
deviceUuid: string,
|
||||||
serviceNames: string[],
|
serviceNames: string[],
|
||||||
) {
|
) {
|
||||||
const { ExpectedError, instanceOf } = await import('../../errors');
|
const { ExpectedError, instanceOf } = await import('../../errors');
|
||||||
@ -116,7 +100,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
// Get device
|
// Get device
|
||||||
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
let device: DeviceWithServiceDetails<CurrentServiceWithCommit>;
|
||||||
try {
|
try {
|
||||||
device = await balena.models.device.getWithServiceDetails(deviceId, {
|
device = await balena.models.device.getWithServiceDetails(deviceUuid, {
|
||||||
$expand: {
|
$expand: {
|
||||||
is_running__release: { $select: 'commit' },
|
is_running__release: { $select: 'commit' },
|
||||||
},
|
},
|
||||||
@ -124,7 +108,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -136,7 +120,7 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
serviceNames.forEach((service) => {
|
serviceNames.forEach((service) => {
|
||||||
if (!device.current_services[service]) {
|
if (!device.current_services[service]) {
|
||||||
throw new ExpectedError(
|
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) {
|
if (serviceContainer) {
|
||||||
restartPromises.push(
|
restartPromises.push(
|
||||||
balena.models.device.restartService(
|
balena.models.device.restartService(
|
||||||
deviceId,
|
deviceUuid,
|
||||||
serviceContainer.image_id,
|
serviceContainer.image_id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -166,32 +150,32 @@ export default class DeviceRestartCmd extends Command {
|
|||||||
await Promise.all(restartPromises);
|
await Promise.all(restartPromises);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message.toLowerCase().includes('no online device')) {
|
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 {
|
} else {
|
||||||
throw e;
|
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.
|
// 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.
|
// 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
|
// Remove this workaround when SDK issue resolved: https://github.com/balena-io/balena-sdk/issues/649
|
||||||
const { instanceOf, ExpectedError } = await import('../../errors');
|
const { instanceOf, ExpectedError } = await import('../../errors');
|
||||||
try {
|
try {
|
||||||
const device = await balena.models.device.get(deviceId);
|
const device = await balena.models.device.get(deviceUuid);
|
||||||
if (!device.is_online) {
|
if (!device.is_online) {
|
||||||
throw new ExpectedError(`Device ${deviceId} is not online.`);
|
throw new ExpectedError(`Device ${deviceUuid} is not online.`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { BalenaDeviceNotFound } = await import('balena-errors');
|
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||||
if (instanceOf(e, BalenaDeviceNotFound)) {
|
if (instanceOf(e, BalenaDeviceNotFound)) {
|
||||||
throw new ExpectedError(`Device ${deviceId} not found.`);
|
throw new ExpectedError(`Device ${deviceUuid} not found.`);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await balena.models.device.restartApplication(deviceId);
|
await balena.models.device.restartApplication(deviceUuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,21 +15,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
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 {
|
export default class DeviceRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -46,18 +35,17 @@ export default class DeviceRmCmd extends Command {
|
|||||||
'$ balena device rm 7cf02a6 --yes',
|
'$ balena device rm 7cf02a6 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description:
|
description:
|
||||||
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
'comma-separated list (no blank spaces) of device UUIDs to be removed',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device rm <uuid(s)>';
|
public static usage = 'device rm <uuid(s)>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -65,9 +53,7 @@ export default class DeviceRmCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(DeviceRmCmd);
|
||||||
DeviceRmCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
@ -84,7 +70,7 @@ export default class DeviceRmCmd extends Command {
|
|||||||
// Remove
|
// Remove
|
||||||
for (const uuid of uuids) {
|
for (const uuid of uuids) {
|
||||||
try {
|
try {
|
||||||
await balena.models.device.remove(tryAsInteger(uuid));
|
await balena.models.device.remove(uuid);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.info(`${err.message}, uuid: ${uuid}`);
|
console.info(`${err.message}, uuid: ${uuid}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
|
@ -15,23 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { IArg } from '@oclif/parser/lib/args';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { tryAsInteger } from '../../utils/validation';
|
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
force: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
uuid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DeviceShutdownCmd extends Command {
|
export default class DeviceShutdownCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Shutdown a device.
|
Shutdown a device.
|
||||||
@ -40,18 +29,16 @@ export default class DeviceShutdownCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = ['$ balena device shutdown 23c73a1'];
|
public static examples = ['$ balena device shutdown 23c73a1'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
uuid: Args.string({
|
||||||
name: 'uuid',
|
|
||||||
description: 'the uuid of the device to shutdown',
|
description: 'the uuid of the device to shutdown',
|
||||||
parse: (dev) => tryAsInteger(dev),
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'device shutdown <uuid>';
|
public static usage = 'device shutdown <uuid>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
force: cf.force,
|
force: cf.force,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -59,9 +46,8 @@ export default class DeviceShutdownCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } =
|
||||||
DeviceShutdownCmd,
|
await this.parse(DeviceShutdownCmd);
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
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,36 +15,25 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { expandForAppName } from '../../utils/helpers';
|
import { expandForAppName } from '../../utils/helpers';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo, jsonInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetFlagMsg,
|
|
||||||
appToFleetOutputMsg,
|
|
||||||
jsonInfo,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
import type { Application } from 'balena-sdk';
|
import type { Device, PineOptions } from 'balena-sdk';
|
||||||
|
|
||||||
interface ExtendedDevice extends DeviceWithDeviceType {
|
const devicesSelectFields = {
|
||||||
dashboard_url?: string;
|
$select: [
|
||||||
application_name?: string | null;
|
'id',
|
||||||
device_type?: string | null;
|
'uuid',
|
||||||
}
|
'device_name',
|
||||||
|
'status',
|
||||||
interface FlagsDef {
|
'is_online',
|
||||||
application?: string;
|
'supervisor_version',
|
||||||
app?: string;
|
'os_version',
|
||||||
fleet?: string;
|
],
|
||||||
help: void;
|
} satisfies PineOptions<Device>;
|
||||||
json: boolean;
|
|
||||||
v13: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DevicesCmd extends Command {
|
export default class DevicesCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -66,84 +55,58 @@ export default class DevicesCmd extends Command {
|
|||||||
|
|
||||||
public static usage = 'devices';
|
public static usage = 'devices';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13()
|
fleet: cf.fleet,
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
application: {
|
|
||||||
...cf.application,
|
|
||||||
exclusive: ['app', 'fleet', 'v13'],
|
|
||||||
},
|
|
||||||
app: { ...cf.app, exclusive: ['application', 'fleet', 'v13'] },
|
|
||||||
}),
|
|
||||||
fleet: { ...cf.fleet, exclusive: ['app', 'application'] },
|
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
v13: cf.v13,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
protected useAppWord = false;
|
|
||||||
protected hasWarned = false;
|
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesCmd);
|
const { flags: options } = await this.parse(DevicesCmd);
|
||||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
const devicesOptions = {
|
||||||
|
...devicesSelectFields,
|
||||||
|
...expandForAppName,
|
||||||
|
$orderby: { device_name: 'asc' },
|
||||||
|
} satisfies PineOptions<Device>;
|
||||||
|
|
||||||
if (
|
const devices = (
|
||||||
(options.application || options.app) &&
|
await (async () => {
|
||||||
!options.json &&
|
if (options.fleet != null) {
|
||||||
process.stderr.isTTY
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
) {
|
const application = await getApplication(balena, options.fleet, {
|
||||||
this.hasWarned = true;
|
$select: 'slug',
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
$expand: {
|
||||||
}
|
owns__device: devicesOptions,
|
||||||
// Consolidate application options
|
},
|
||||||
options.application ||= options.app || options.fleet;
|
});
|
||||||
|
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) {
|
const fields: Array<keyof (typeof devices)[number]> = [
|
||||||
const { getApplication } = await import('../../utils/sdk');
|
|
||||||
const application = await getApplication(balena, options.application);
|
|
||||||
devices = (await balena.models.device.getAllByApplication(
|
|
||||||
application.id,
|
|
||||||
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 jName = this.useAppWord ? 'application_name' : 'fleet_name';
|
|
||||||
const tName = this.useAppWord ? 'APPLICATION NAME' : 'FLEET';
|
|
||||||
const fields = [
|
|
||||||
'id',
|
'id',
|
||||||
'uuid',
|
'uuid',
|
||||||
'device_name',
|
'device_name',
|
||||||
'device_type',
|
'device_type',
|
||||||
options.json
|
'fleet',
|
||||||
? `application_name => ${jName}`
|
|
||||||
: `application_name => ${tName}`,
|
|
||||||
'status',
|
'status',
|
||||||
'is_online',
|
'is_online',
|
||||||
'supervisor_version',
|
'supervisor_version',
|
||||||
@ -156,9 +119,6 @@ export default class DevicesCmd extends Command {
|
|||||||
const mapped = devices.map((device) => pickAndRename(device, fields));
|
const mapped = devices.map((device) => pickAndRename(device, fields));
|
||||||
console.log(JSON.stringify(mapped, null, 4));
|
console.log(JSON.stringify(mapped, null, 4));
|
||||||
} else {
|
} else {
|
||||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetOutputMsg));
|
|
||||||
}
|
|
||||||
const _ = await import('lodash');
|
const _ = await import('lodash');
|
||||||
console.log(
|
console.log(
|
||||||
getVisuals().table.horizontal(
|
getVisuals().table.horizontal(
|
||||||
|
@ -14,40 +14,21 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flags } from '@oclif/command';
|
import { Flags } from '@oclif/core';
|
||||||
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { CommandHelp } from '../../utils/oclif-utils';
|
import { CommandHelp } from '../../utils/oclif-utils';
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
discontinued: boolean;
|
|
||||||
help: void;
|
|
||||||
json?: boolean;
|
|
||||||
verbose?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deprecatedInfo = isV13()
|
|
||||||
? ''
|
|
||||||
: `
|
|
||||||
The --verbose option may add extra columns/fields to the output. Currently
|
|
||||||
this includes the "STATE" column which is DEPRECATED and whose values are one
|
|
||||||
of 'new', 'released' or 'discontinued'. However, 'discontinued' device types
|
|
||||||
are only listed if the '--discontinued' option is also used, and this option
|
|
||||||
is also DEPRECATED.
|
|
||||||
`
|
|
||||||
.split('\n')
|
|
||||||
.join(`\n\t\t`);
|
|
||||||
|
|
||||||
export default class DevicesSupportedCmd extends Command {
|
export default class DevicesSupportedCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||||
|
|
||||||
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
List the supported device types (like 'raspberrypi3' or 'intel-nuc').
|
||||||
${deprecatedInfo}
|
|
||||||
The --json option is recommended when scripting the output of this command,
|
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
|
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
|
types like lists and empty strings (for example, the ALIASES column contains a
|
||||||
@ -56,8 +37,7 @@ export default class DevicesSupportedCmd extends Command {
|
|||||||
`;
|
`;
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena devices supported',
|
'$ balena devices supported',
|
||||||
'$ balena devices supported --verbose',
|
'$ balena devices supported --json',
|
||||||
'$ balena devices supported -vj',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usage = (
|
public static usage = (
|
||||||
@ -65,83 +45,49 @@ export default class DevicesSupportedCmd extends Command {
|
|||||||
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
new CommandHelp({ args: DevicesSupportedCmd.args }).defaultUsage()
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
discontinued: flags.boolean({
|
|
||||||
description: isV13()
|
|
||||||
? 'No effect (DEPRECATED)'
|
|
||||||
: 'include "discontinued" device types (DEPRECATED)',
|
|
||||||
}),
|
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
json: flags.boolean({
|
json: Flags.boolean({
|
||||||
char: 'j',
|
char: 'j',
|
||||||
description: 'produce JSON output instead of tabular output',
|
description: 'produce JSON output instead of tabular output',
|
||||||
}),
|
}),
|
||||||
verbose: flags.boolean({
|
|
||||||
char: 'v',
|
|
||||||
description: isV13()
|
|
||||||
? 'No effect (DEPRECATED)'
|
|
||||||
: 'add extra columns in the tabular output (DEPRECATED)',
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
|
const { flags: options } = await this.parse(DevicesSupportedCmd);
|
||||||
const [dts, configDTs] = await Promise.all([
|
const pineOptions = {
|
||||||
getBalenaSdk().models.deviceType.getAllSupported({
|
$select: ['slug', 'name'],
|
||||||
$expand: { is_of__cpu_architecture: { $select: 'slug' } },
|
$expand: {
|
||||||
$select: ['slug', 'name'],
|
is_of__cpu_architecture: { $select: 'slug' },
|
||||||
}),
|
device_type_alias: {
|
||||||
getBalenaSdk().models.config.getDeviceTypes(),
|
$select: 'is_referenced_by__alias',
|
||||||
]);
|
$orderby: { is_referenced_by__alias: 'asc' },
|
||||||
const dtsBySlug = _.keyBy(dts, (dt) => dt.slug);
|
},
|
||||||
const configDTsBySlug = _.keyBy(configDTs, (dt) => dt.slug);
|
},
|
||||||
const discontinuedDTs = isV13()
|
} satisfies BalenaSdk.PineOptions<BalenaSdk.DeviceType>;
|
||||||
? []
|
const dts = (await getBalenaSdk().models.deviceType.getAllSupported(
|
||||||
: configDTs.filter((dt) => dt.state === 'DISCONTINUED');
|
pineOptions,
|
||||||
const discontinuedDTsBySlug = _.keyBy(discontinuedDTs, (dt) => dt.slug);
|
)) as Array<
|
||||||
// set of slugs from models.deviceType.getAllSupported() plus slugs of
|
BalenaSdk.PineTypedResult<BalenaSdk.DeviceType, typeof pineOptions>
|
||||||
// discontinued device types as per models.config.getDeviceTypes()
|
>;
|
||||||
const slugsOfInterest = new Set([
|
|
||||||
...Object.keys(dtsBySlug),
|
|
||||||
...Object.keys(discontinuedDTsBySlug),
|
|
||||||
]);
|
|
||||||
interface DT {
|
interface DT {
|
||||||
slug: string;
|
slug: string;
|
||||||
aliases: string[];
|
aliases: string[];
|
||||||
arch: string;
|
arch: string;
|
||||||
state?: string; // to be removed in CLI v13
|
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
let deviceTypes: DT[] = [];
|
let deviceTypes = dts.map((dt): DT => {
|
||||||
for (const slug of slugsOfInterest) {
|
const aliases = dt.device_type_alias
|
||||||
const configDT: Partial<typeof configDTs[0]> =
|
.map((dta) => dta.is_referenced_by__alias)
|
||||||
configDTsBySlug[slug] || {};
|
.filter((alias) => alias !== dt.slug);
|
||||||
if (configDT.state === 'DISCONTINUED' && !options.discontinued) {
|
return {
|
||||||
continue;
|
slug: dt.slug,
|
||||||
}
|
|
||||||
const dt: Partial<typeof dts[0]> = dtsBySlug[slug] || {};
|
|
||||||
const aliases = (configDT.aliases || []).filter(
|
|
||||||
(alias) => alias !== slug,
|
|
||||||
);
|
|
||||||
deviceTypes.push({
|
|
||||||
slug,
|
|
||||||
aliases: options.json ? aliases : [aliases.join(', ')],
|
aliases: options.json ? aliases : [aliases.join(', ')],
|
||||||
arch:
|
arch: dt.is_of__cpu_architecture[0]?.slug || 'n/a',
|
||||||
(dt.is_of__cpu_architecture as any)?.[0]?.slug ||
|
name: dt.name || 'N/A',
|
||||||
configDT.arch ||
|
};
|
||||||
'n/a',
|
});
|
||||||
// 'BETA' renamed to 'NEW'
|
const fields = ['slug', 'aliases', 'arch', 'name'];
|
||||||
// https://www.flowdock.com/app/rulemotion/i-cli/threads/1svvyaf8FAZeSdG4dPJc4kHOvJU
|
|
||||||
state: isV13()
|
|
||||||
? undefined
|
|
||||||
: (configDT.state || 'NEW').replace('BETA', 'NEW'),
|
|
||||||
name: dt.name || configDT.name || 'N/A',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const fields =
|
|
||||||
options.verbose && !isV13()
|
|
||||||
? ['slug', 'aliases', 'arch', 'state', 'name']
|
|
||||||
: ['slug', 'aliases', 'arch', 'name'];
|
|
||||||
deviceTypes = _.sortBy(deviceTypes, fields);
|
deviceTypes = _.sortBy(deviceTypes, fields);
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
console.log(JSON.stringify(deviceTypes, null, 4));
|
console.log(JSON.stringify(deviceTypes, null, 4));
|
||||||
|
87
lib/commands/env/add.ts
vendored
87
lib/commands/env/add.ts
vendored
@ -15,21 +15,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type * as BalenaSdk from 'balena-sdk';
|
import type * as BalenaSdk from 'balena-sdk';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetFlagMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../../utils/messages';
|
|
||||||
import { isV13 } from '../../utils/version';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
application?: string;
|
|
||||||
fleet?: string;
|
fleet?: string;
|
||||||
device?: string; // device UUID
|
device?: string; // device UUID
|
||||||
help: void;
|
help: void;
|
||||||
@ -84,44 +78,35 @@ export default class EnvAddCmd extends Command {
|
|||||||
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
'$ balena env add EDITOR vim --device 7cf02a6,d6f1433 --service MyService,MyService2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
|
||||||
required: true,
|
required: true,
|
||||||
description: 'environment or config variable name',
|
description: 'environment or config variable name',
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
required: false,
|
required: false,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"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 usage = 'env add <name> [value]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13()
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
? {}
|
device: { ...cf.device, exclusive: ['fleet'] },
|
||||||
: { application: { ...cf.application, exclusive: ['fleet', 'device'] } }),
|
|
||||||
fleet: { ...cf.fleet, exclusive: ['application', 'device'] },
|
|
||||||
device: { ...cf.device, exclusive: ['application', 'fleet'] },
|
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
quiet: cf.quiet,
|
quiet: cf.quiet,
|
||||||
service: cf.service,
|
service: cf.service,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(EnvAddCmd);
|
||||||
EnvAddCmd,
|
|
||||||
);
|
|
||||||
const cmd = this;
|
const cmd = this;
|
||||||
|
|
||||||
if (options.application && process.stderr.isTTY) {
|
if (!options.fleet && !options.device) {
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
|
||||||
}
|
|
||||||
options.application ||= options.fleet;
|
|
||||||
if (!options.application && !options.device) {
|
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
'Either the --fleet or the --device option must be specified',
|
'Either the --fleet or the --device option must be specified',
|
||||||
);
|
);
|
||||||
@ -163,16 +148,16 @@ export default class EnvAddCmd extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const varType = isConfigVar ? 'configVar' : 'envVar';
|
const varType = isConfigVar ? 'configVar' : 'envVar';
|
||||||
if (options.application) {
|
if (options.fleet) {
|
||||||
for (const app of options.application.split(',')) {
|
for (const appSlug of await resolveFleetSlugs(balena, options.fleet)) {
|
||||||
try {
|
try {
|
||||||
await balena.models.application[varType].set(
|
await balena.models.application[varType].set(
|
||||||
app,
|
appSlug,
|
||||||
params.name,
|
params.name,
|
||||||
params.value,
|
params.value,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${app}`);
|
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,6 +178,25 @@ 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 fleet.
|
* Add service variables for a device or fleet.
|
||||||
*/
|
*/
|
||||||
@ -201,18 +205,18 @@ async function setServiceVars(
|
|||||||
params: ArgsDef,
|
params: ArgsDef,
|
||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
) {
|
) {
|
||||||
if (options.application) {
|
if (options.fleet) {
|
||||||
for (const app of options.application.split(',')) {
|
for (const appSlug of await resolveFleetSlugs(sdk, options.fleet)) {
|
||||||
for (const service of options.service!.split(',')) {
|
for (const service of options.service!.split(',')) {
|
||||||
try {
|
try {
|
||||||
const serviceId = await getServiceIdForApp(sdk, app, service);
|
const serviceId = await getServiceIdForApp(sdk, appSlug, service);
|
||||||
await sdk.models.service.var.set(
|
await sdk.models.service.var.set(
|
||||||
serviceId,
|
serviceId,
|
||||||
params.name,
|
params.name,
|
||||||
params.value!,
|
params.value!,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`${err.message}, fleet: ${app}`);
|
console.error(`${err.message}, fleet: ${appSlug}`);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,11 +261,12 @@ async function setServiceVars(
|
|||||||
*/
|
*/
|
||||||
async function getServiceIdForApp(
|
async function getServiceIdForApp(
|
||||||
sdk: BalenaSdk.BalenaSDK,
|
sdk: BalenaSdk.BalenaSDK,
|
||||||
appName: string,
|
appSlug: string,
|
||||||
serviceName: string,
|
serviceName: string,
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let serviceId: number | undefined;
|
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 },
|
$filter: { service_name: serviceName },
|
||||||
});
|
});
|
||||||
if (services.length > 0) {
|
if (services.length > 0) {
|
||||||
@ -269,7 +274,7 @@ async function getServiceIdForApp(
|
|||||||
}
|
}
|
||||||
if (serviceId === undefined) {
|
if (serviceId === undefined) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Cannot find service ${serviceName} for fleet ${appName}`,
|
`Cannot find service ${serviceName} for fleet ${appSlug}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return serviceId;
|
return serviceId;
|
||||||
|
38
lib/commands/env/rename.ts
vendored
38
lib/commands/env/rename.ts
vendored
@ -14,7 +14,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
@ -22,20 +22,6 @@ import * as ec from '../../utils/env-common';
|
|||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
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 {
|
export default class EnvRenameCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Change the value of a config or env var for a fleet, device or service.
|
Change the value of a config or env var for a fleet, device or service.
|
||||||
@ -54,24 +40,22 @@ export default class EnvRenameCmd extends Command {
|
|||||||
'$ balena env rename 678678 1 --device --config',
|
'$ balena env rename 678678 1 --device --config',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
id: Args.integer({
|
||||||
name: 'id',
|
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
description: "variable's numeric database ID",
|
||||||
parse: (input) => parseAsInteger(input, 'id'),
|
parse: async (input) => parseAsInteger(input, 'id'),
|
||||||
},
|
}),
|
||||||
{
|
value: Args.string({
|
||||||
name: 'value',
|
|
||||||
required: true,
|
required: true,
|
||||||
description:
|
description:
|
||||||
"variable value; if omitted, use value from this process' environment",
|
"variable value; if omitted, use value from this process' environment",
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'env rename <id> <value>';
|
public static usage = 'env rename <id> <value>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
config: ec.booleanConfig,
|
config: ec.booleanConfig,
|
||||||
device: ec.booleanDevice,
|
device: ec.booleanDevice,
|
||||||
service: ec.booleanService,
|
service: ec.booleanService,
|
||||||
@ -79,9 +63,7 @@ export default class EnvRenameCmd extends Command {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: opt } = await this.parse(EnvRenameCmd);
|
||||||
EnvRenameCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Command.checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
|
34
lib/commands/env/rm.ts
vendored
34
lib/commands/env/rm.ts
vendored
@ -15,26 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
|
|
||||||
import * as ec from '../../utils/env-common';
|
import * as ec from '../../utils/env-common';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
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 {
|
export default class EnvRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Remove a config or env var from a fleet, device or service.
|
Remove a config or env var from a fleet, device or service.
|
||||||
@ -57,22 +44,21 @@ export default class EnvRmCmd extends Command {
|
|||||||
'$ balena env rm 789789 --device --service --yes',
|
'$ balena env rm 789789 --device --service --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
id: Args.integer({
|
||||||
name: 'id',
|
|
||||||
required: true,
|
required: true,
|
||||||
description: "variable's numeric database ID",
|
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 usage = 'env rm <id>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
config: ec.booleanConfig,
|
config: ec.booleanConfig,
|
||||||
device: ec.booleanDevice,
|
device: ec.booleanDevice,
|
||||||
service: ec.booleanService,
|
service: ec.booleanService,
|
||||||
yes: flags.boolean({
|
yes: Flags.boolean({
|
||||||
char: 'y',
|
char: 'y',
|
||||||
description:
|
description:
|
||||||
'do not prompt for confirmation before deleting the variable',
|
'do not prompt for confirmation before deleting the variable',
|
||||||
@ -81,9 +67,7 @@ export default class EnvRmCmd extends Command {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: opt } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: opt } = await this.parse(EnvRmCmd);
|
||||||
EnvRmCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
await Command.checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
|
@ -14,49 +14,34 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 type * as SDK from 'balena-sdk';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import {
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
applicationIdInfo,
|
|
||||||
appToFleetFlagMsg,
|
|
||||||
appToFleetOutputMsg,
|
|
||||||
warnify,
|
|
||||||
} from '../utils/messages';
|
|
||||||
import { isV13 } from '../utils/version';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
type FlagsDef = Interfaces.InferredFlags<typeof EnvsCmd.flags>;
|
||||||
application?: string;
|
|
||||||
fleet?: string;
|
|
||||||
config: boolean;
|
|
||||||
device?: string; // device UUID
|
|
||||||
json: boolean;
|
|
||||||
help: void;
|
|
||||||
service?: string; // service name
|
|
||||||
verbose: boolean;
|
|
||||||
v13: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
interface EnvironmentVariableInfo extends SDK.EnvironmentVariableBase {
|
||||||
appName?: string | null; // application name
|
fleet?: string | null; // fleet slug
|
||||||
deviceUUID?: string; // device UUID
|
deviceUUID?: string; // device UUID
|
||||||
serviceName?: string; // service name
|
serviceName?: string; // service name
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeviceServiceEnvironmentVariableInfo
|
interface DeviceServiceEnvironmentVariableInfo
|
||||||
extends SDK.DeviceServiceEnvironmentVariable {
|
extends SDK.DeviceServiceEnvironmentVariable {
|
||||||
appName?: string; // application name
|
fleet?: string; // fleet slug
|
||||||
deviceUUID?: string; // device UUID
|
deviceUUID?: string; // device UUID
|
||||||
serviceName?: string; // service name
|
serviceName?: string; // service name
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceEnvironmentVariableInfo
|
interface ServiceEnvironmentVariableInfo
|
||||||
extends SDK.ServiceEnvironmentVariable {
|
extends SDK.ServiceEnvironmentVariable {
|
||||||
appName?: string; // application name
|
fleet?: string; // fleet slug
|
||||||
deviceUUID?: string; // device UUID
|
deviceUUID?: string; // device UUID
|
||||||
serviceName?: string; // service name
|
serviceName?: string; // service name
|
||||||
}
|
}
|
||||||
@ -96,15 +81,12 @@ export default class EnvsCmd extends Command {
|
|||||||
in case the current user was removed from the fleet by the fleet's owner).
|
in case the current user was removed from the fleet by the fleet's owner).
|
||||||
|
|
||||||
${applicationIdInfo.split('\n').join('\n\t\t')}
|
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||||
|
|
||||||
${appToFleetOutputMsg.split('\n').join('\n\t\t')}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static examples = [
|
public static examples = [
|
||||||
'$ balena envs --fleet myorg/myfleet',
|
'$ balena envs --fleet myorg/myfleet',
|
||||||
'$ balena envs --fleet MyFleet --json',
|
'$ balena envs --fleet MyFleet --json',
|
||||||
'$ balena envs --fleet MyFleet --service MyService',
|
'$ balena envs --fleet MyFleet --service MyService',
|
||||||
'$ balena envs --fleet MyFleet --service MyService',
|
|
||||||
'$ balena envs --fleet MyFleet --config',
|
'$ balena envs --fleet MyFleet --config',
|
||||||
'$ balena envs --device 7cf02a6',
|
'$ balena envs --device 7cf02a6',
|
||||||
'$ balena envs --device 7cf02a6 --json',
|
'$ balena envs --device 7cf02a6 --json',
|
||||||
@ -114,62 +96,44 @@ export default class EnvsCmd extends Command {
|
|||||||
|
|
||||||
public static usage = 'envs';
|
public static usage = 'envs';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13()
|
fleet: { ...cf.fleet, exclusive: ['device'] },
|
||||||
? {}
|
config: Flags.boolean({
|
||||||
: {
|
|
||||||
all: flags.boolean({
|
|
||||||
default: false,
|
|
||||||
description: 'No-op since balena CLI v12.0.0.',
|
|
||||||
hidden: true,
|
|
||||||
}),
|
|
||||||
application: {
|
|
||||||
exclusive: ['device', 'fleet', 'v13'],
|
|
||||||
...cf.application,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
fleet: { exclusive: ['device', 'application'], ...cf.fleet },
|
|
||||||
config: flags.boolean({
|
|
||||||
default: false,
|
default: false,
|
||||||
char: 'c',
|
char: 'c',
|
||||||
description: 'show configuration variables only',
|
description: 'show configuration variables only',
|
||||||
exclusive: ['service'],
|
exclusive: ['service'],
|
||||||
}),
|
}),
|
||||||
device: { exclusive: ['fleet', 'application'], ...cf.device },
|
device: { ...cf.device, exclusive: ['fleet'] },
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
json: cf.json,
|
json: cf.json,
|
||||||
verbose: cf.verbose,
|
service: { ...cf.service, exclusive: ['config'] },
|
||||||
service: { exclusive: ['config'], ...cf.service },
|
|
||||||
v13: cf.v13,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected useAppWord = false;
|
|
||||||
protected hasWarned = false;
|
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options } = this.parse<FlagsDef, {}>(EnvsCmd);
|
const { flags: options } = await this.parse(EnvsCmd);
|
||||||
this.useAppWord = !options.fleet && !options.v13 && !isV13();
|
|
||||||
|
|
||||||
const variables: EnvironmentVariableInfo[] = [];
|
const variables: EnvironmentVariableInfo[] = [];
|
||||||
|
|
||||||
await Command.checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
|
|
||||||
if (options.application && !options.json && process.stderr.isTTY) {
|
if (!options.fleet && !options.device) {
|
||||||
this.hasWarned = true;
|
|
||||||
console.error(warnify(appToFleetFlagMsg));
|
|
||||||
}
|
|
||||||
options.application ||= options.fleet;
|
|
||||||
if (!options.application && !options.device) {
|
|
||||||
throw new ExpectedError('Missing --fleet or --device option');
|
throw new ExpectedError('Missing --fleet or --device option');
|
||||||
}
|
}
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
let appNameOrSlug = 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
|
let fullUUID: string | undefined; // as oppposed to the short, 7-char UUID
|
||||||
|
|
||||||
if (options.device) {
|
if (options.device) {
|
||||||
const { getDeviceAndMaybeAppFromUUID } = await import('../utils/cloud');
|
const { getDeviceAndMaybeAppFromUUID } = await import(
|
||||||
|
'../../utils/cloud'
|
||||||
|
);
|
||||||
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
const [device, app] = await getDeviceAndMaybeAppFromUUID(
|
||||||
balena,
|
balena,
|
||||||
options.device,
|
options.device,
|
||||||
@ -178,23 +142,23 @@ export default class EnvsCmd extends Command {
|
|||||||
);
|
);
|
||||||
fullUUID = device.uuid;
|
fullUUID = device.uuid;
|
||||||
if (app) {
|
if (app) {
|
||||||
appNameOrSlug = app.slug;
|
fleetSlug = app.slug;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (appNameOrSlug && options.service) {
|
if (fleetSlug && options.service) {
|
||||||
await validateServiceName(balena, options.service, appNameOrSlug);
|
await validateServiceName(balena, options.service, fleetSlug);
|
||||||
}
|
}
|
||||||
variables.push(...(await getAppVars(balena, appNameOrSlug, options)));
|
variables.push(...(await getAppVars(balena, fleetSlug, options)));
|
||||||
if (fullUUID) {
|
if (fullUUID) {
|
||||||
variables.push(
|
variables.push(
|
||||||
...(await getDeviceVars(balena, fullUUID, appNameOrSlug, options)),
|
...(await getDeviceVars(balena, fullUUID, fleetSlug, options)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!options.json && variables.length === 0) {
|
if (!options.json && variables.length === 0) {
|
||||||
const target =
|
const target =
|
||||||
(options.service ? `service "${options.service}" of ` : '') +
|
(options.service ? `service "${options.service}" of ` : '') +
|
||||||
(options.application
|
(options.fleet
|
||||||
? `fleet "${options.application}"`
|
? `fleet "${options.fleet}"`
|
||||||
: `device "${options.device}"`);
|
: `device "${options.device}"`);
|
||||||
throw new ExpectedError(`No environment variables found for ${target}`);
|
throw new ExpectedError(`No environment variables found for ${target}`);
|
||||||
}
|
}
|
||||||
@ -206,24 +170,14 @@ export default class EnvsCmd extends Command {
|
|||||||
varArray: EnvironmentVariableInfo[],
|
varArray: EnvironmentVariableInfo[],
|
||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
) {
|
) {
|
||||||
const fields = ['id', 'name', 'value'];
|
const fields = ['id', 'name', 'value', 'fleet'];
|
||||||
|
|
||||||
// Replace undefined app names with 'N/A' or null
|
// Replace undefined app names with 'N/A' or null
|
||||||
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
varArray = varArray.map((i: EnvironmentVariableInfo) => {
|
||||||
if (i.appName) {
|
i.fleet ||= options.json ? null : 'N/A';
|
||||||
// use slug in v13, app name in v12 for compatibility
|
|
||||||
i.appName = isV13()
|
|
||||||
? i.appName
|
|
||||||
: i.appName.substring(i.appName.indexOf('/') + 1);
|
|
||||||
} else {
|
|
||||||
i.appName = options.json ? null : 'N/A';
|
|
||||||
}
|
|
||||||
return i;
|
return i;
|
||||||
});
|
});
|
||||||
|
|
||||||
const jName = this.useAppWord ? 'appName' : 'fleetName';
|
|
||||||
const tName = this.useAppWord ? 'APPLICATION' : 'FLEET';
|
|
||||||
fields.push(options.json ? `appName => ${jName}` : `appName => ${tName}`);
|
|
||||||
if (options.device) {
|
if (options.device) {
|
||||||
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
fields.push(options.json ? 'deviceUUID' : 'deviceUUID => DEVICE');
|
||||||
}
|
}
|
||||||
@ -232,13 +186,10 @@ export default class EnvsCmd extends Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.json) {
|
if (options.json) {
|
||||||
const { pickAndRename } = await import('../utils/helpers');
|
const { pickAndRename } = await import('../../utils/helpers');
|
||||||
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
const mapped = varArray.map((o) => pickAndRename(o, fields));
|
||||||
this.log(JSON.stringify(mapped, null, 4));
|
this.log(JSON.stringify(mapped, null, 4));
|
||||||
} else {
|
} else {
|
||||||
if (!this.hasWarned && this.useAppWord && process.stderr.isTTY) {
|
|
||||||
console.error(warnify(appToFleetOutputMsg));
|
|
||||||
}
|
|
||||||
this.log(
|
this.log(
|
||||||
getVisuals().table.horizontal(
|
getVisuals().table.horizontal(
|
||||||
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
_.sortBy(varArray, (v: SDK.EnvironmentVariableBase) => v.name),
|
||||||
@ -252,14 +203,15 @@ export default class EnvsCmd extends Command {
|
|||||||
async function validateServiceName(
|
async function validateServiceName(
|
||||||
sdk: SDK.BalenaSDK,
|
sdk: SDK.BalenaSDK,
|
||||||
serviceName: string,
|
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 },
|
$filter: { service_name: serviceName },
|
||||||
});
|
});
|
||||||
if (services.length === 0) {
|
if (services.length === 0) {
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Service "${serviceName}" not found for fleet "${appName}"`,
|
`Service "${serviceName}" not found for fleet "${fleetSlug}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,17 +225,18 @@ async function validateServiceName(
|
|||||||
*/
|
*/
|
||||||
async function getAppVars(
|
async function getAppVars(
|
||||||
sdk: SDK.BalenaSDK,
|
sdk: SDK.BalenaSDK,
|
||||||
appNameOrSlug: string | undefined,
|
fleetSlug: string | undefined,
|
||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
): Promise<EnvironmentVariableInfo[]> {
|
): Promise<EnvironmentVariableInfo[]> {
|
||||||
const appVars: EnvironmentVariableInfo[] = [];
|
const appVars: EnvironmentVariableInfo[] = [];
|
||||||
if (!appNameOrSlug) {
|
if (!fleetSlug) {
|
||||||
return appVars;
|
return appVars;
|
||||||
}
|
}
|
||||||
const vars = await sdk.models.application[
|
const vars =
|
||||||
options.config ? 'configVar' : 'envVar'
|
await sdk.models.application[
|
||||||
].getAllByApplication(appNameOrSlug);
|
options.config ? 'configVar' : 'envVar'
|
||||||
fillInInfoFields(vars, appNameOrSlug);
|
].getAllByApplication(fleetSlug);
|
||||||
|
fillInInfoFields(vars, fleetSlug);
|
||||||
appVars.push(...vars);
|
appVars.push(...vars);
|
||||||
if (!options.config) {
|
if (!options.config) {
|
||||||
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
|
const pineOpts: SDK.PineOptions<SDK.ServiceEnvironmentVariable> = {
|
||||||
@ -299,10 +252,10 @@ async function getAppVars(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
const serviceVars = await sdk.models.service.var.getAllByApplication(
|
||||||
appNameOrSlug,
|
fleetSlug,
|
||||||
pineOpts,
|
pineOpts,
|
||||||
);
|
);
|
||||||
fillInInfoFields(serviceVars, appNameOrSlug);
|
fillInInfoFields(serviceVars, fleetSlug);
|
||||||
appVars.push(...serviceVars);
|
appVars.push(...serviceVars);
|
||||||
}
|
}
|
||||||
return appVars;
|
return appVars;
|
||||||
@ -315,16 +268,15 @@ async function getAppVars(
|
|||||||
async function getDeviceVars(
|
async function getDeviceVars(
|
||||||
sdk: SDK.BalenaSDK,
|
sdk: SDK.BalenaSDK,
|
||||||
fullUUID: string,
|
fullUUID: string,
|
||||||
appNameOrSlug: string | undefined,
|
fleetSlug: string | undefined,
|
||||||
options: FlagsDef,
|
options: FlagsDef,
|
||||||
): Promise<EnvironmentVariableInfo[]> {
|
): Promise<EnvironmentVariableInfo[]> {
|
||||||
const printedUUID = options.json ? fullUUID : options.device!;
|
const printedUUID = options.json ? fullUUID : options.device!;
|
||||||
const deviceVars: EnvironmentVariableInfo[] = [];
|
const deviceVars: EnvironmentVariableInfo[] = [];
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
const deviceConfigVars = await sdk.models.device.configVar.getAllByDevice(
|
const deviceConfigVars =
|
||||||
fullUUID,
|
await sdk.models.device.configVar.getAllByDevice(fullUUID);
|
||||||
);
|
fillInInfoFields(deviceConfigVars, fleetSlug, printedUUID);
|
||||||
fillInInfoFields(deviceConfigVars, appNameOrSlug, printedUUID);
|
|
||||||
deviceVars.push(...deviceConfigVars);
|
deviceVars.push(...deviceConfigVars);
|
||||||
} else {
|
} else {
|
||||||
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
|
const pineOpts: SDK.PineOptions<SDK.DeviceServiceEnvironmentVariable> = {
|
||||||
@ -345,13 +297,12 @@ async function getDeviceVars(
|
|||||||
fullUUID,
|
fullUUID,
|
||||||
pineOpts,
|
pineOpts,
|
||||||
);
|
);
|
||||||
fillInInfoFields(deviceServiceVars, appNameOrSlug, printedUUID);
|
fillInInfoFields(deviceServiceVars, fleetSlug, printedUUID);
|
||||||
deviceVars.push(...deviceServiceVars);
|
deviceVars.push(...deviceServiceVars);
|
||||||
|
|
||||||
const deviceEnvVars = await sdk.models.device.envVar.getAllByDevice(
|
const deviceEnvVars =
|
||||||
fullUUID,
|
await sdk.models.device.envVar.getAllByDevice(fullUUID);
|
||||||
);
|
fillInInfoFields(deviceEnvVars, fleetSlug, printedUUID);
|
||||||
fillInInfoFields(deviceEnvVars, appNameOrSlug, printedUUID);
|
|
||||||
deviceVars.push(...deviceEnvVars);
|
deviceVars.push(...deviceEnvVars);
|
||||||
}
|
}
|
||||||
return deviceVars;
|
return deviceVars;
|
||||||
@ -367,7 +318,7 @@ function fillInInfoFields(
|
|||||||
| EnvironmentVariableInfo[]
|
| EnvironmentVariableInfo[]
|
||||||
| DeviceServiceEnvironmentVariableInfo[]
|
| DeviceServiceEnvironmentVariableInfo[]
|
||||||
| ServiceEnvironmentVariableInfo[],
|
| ServiceEnvironmentVariableInfo[],
|
||||||
appNameOrSlug?: string,
|
fleetSlug?: string,
|
||||||
deviceUUID?: string,
|
deviceUUID?: string,
|
||||||
) {
|
) {
|
||||||
for (const envVar of varArray) {
|
for (const envVar of varArray) {
|
||||||
@ -381,7 +332,7 @@ function fillInInfoFields(
|
|||||||
?.installs__service as SDK.Service[]
|
?.installs__service as SDK.Service[]
|
||||||
)[0]?.service_name;
|
)[0]?.service_name;
|
||||||
}
|
}
|
||||||
envVar.appName = appNameOrSlug;
|
envVar.fleet = fleetSlug;
|
||||||
envVar.serviceName = envVar.serviceName || '*';
|
envVar.serviceName = envVar.serviceName || '*';
|
||||||
envVar.deviceUUID = deviceUUID || '*';
|
envVar.deviceUUID = deviceUUID || '*';
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2021 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,69 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetCreateCmd } from '../app/create';
|
import { Flags, Args } from '@oclif/core';
|
||||||
|
|
||||||
export default FleetCreateCmd;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2021 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,79 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetCmd } from '../app';
|
import { Flags } from '@oclif/core';
|
||||||
|
|
||||||
export default FleetCmd;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2020 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,61 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetPurgeCmd } from '../app/purge';
|
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 FleetPurgeCmd;
|
export default class FleetPurgeCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Purge data from a fleet.
|
||||||
|
|
||||||
|
Purge data from all devices belonging to a fleet.
|
||||||
|
This will clear the fleet's '/data' directory.
|
||||||
|
|
||||||
|
${applicationIdInfo.split('\n').join('\n\t\t')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
public static examples = [
|
||||||
|
'$ balena fleet purge MyFleet',
|
||||||
|
'$ balena fleet purge myorg/myfleet',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
fleet: ca.fleetRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static usage = 'fleet purge <fleet>';
|
||||||
|
|
||||||
|
public static flags = {
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
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,
|
||||||
|
const application = await getApplication(balena, params.fleet, {
|
||||||
|
$select: 'id',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
// ignore in this case.
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2020 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,125 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetRenameCmd } from '../app/rename';
|
import { Args } from '@oclif/core';
|
||||||
|
|
||||||
export default FleetRenameCmd;
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2020 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,50 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetRestartCmd } from '../app/restart';
|
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 FleetRestartCmd;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2021 Balena Ltd.
|
* Copyright 2016-2020 Balena Ltd.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,6 +15,61 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FleetRmCmd } from '../app/rm';
|
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 FleetRmCmd;
|
export default class FleetRmCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Remove a fleet.
|
||||||
|
|
||||||
|
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 fleet rm MyFleet',
|
||||||
|
'$ balena fleet rm MyFleet --yes',
|
||||||
|
'$ balena fleet rm myorg/myfleet',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
fleet: ca.fleetRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static usage = 'fleet rm <fleet>';
|
||||||
|
|
||||||
|
public static flags = {
|
||||||
|
yes: cf.yes,
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params, flags: options } = await this.parse(FleetRmCmd);
|
||||||
|
|
||||||
|
const { confirm } = await import('../../utils/patterns');
|
||||||
|
const { getApplication } = await import('../../utils/sdk');
|
||||||
|
const balena = getBalenaSdk();
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
await confirm(
|
||||||
|
options.yes ?? false,
|
||||||
|
`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 balena.models.application.remove(application.slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
56
lib/commands/fleet/track-latest.ts
Normal file
56
lib/commands/fleet/track-latest.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2016-2020 Balena Ltd.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Args } from '@oclif/core';
|
||||||
|
import Command from '../../command';
|
||||||
|
import * as cf from '../../utils/common-flags';
|
||||||
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
|
export default class FleetTrackLatestCmd extends Command {
|
||||||
|
public static description = stripIndent`
|
||||||
|
Make this fleet track the latest release.
|
||||||
|
|
||||||
|
Make this fleet track the latest release.
|
||||||
|
`;
|
||||||
|
public static examples = [
|
||||||
|
'$ balena fleet track-latest myorg/myfleet',
|
||||||
|
'$ balena fleet track-latest myfleet',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static args = {
|
||||||
|
slug: Args.string({
|
||||||
|
description: 'the slug of the fleet to make track the latest release',
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static usage = 'fleet track-latest <slug>';
|
||||||
|
|
||||||
|
public static flags = {
|
||||||
|
help: cf.help,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static authenticated = true;
|
||||||
|
|
||||||
|
public async run() {
|
||||||
|
const { args: params } = await this.parse(FleetTrackLatestCmd);
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
import { CommandHelp } from '../../utils/oclif-utils';
|
import { CommandHelp } from '../../utils/oclif-utils';
|
||||||
@ -27,12 +28,6 @@ import { CommandHelp } from '../../utils/oclif-utils';
|
|||||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308357
|
||||||
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
// - https://github.com/balena-io/balena-cli/pull/1455#discussion_r334308526
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
image: string;
|
|
||||||
type: string;
|
|
||||||
config: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class OsinitCmd extends Command {
|
export default class OsinitCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Do actual init of the device with the preconfigured os image.
|
Do actual init of the device with the preconfigured os image.
|
||||||
@ -41,20 +36,17 @@ export default class OsinitCmd extends Command {
|
|||||||
Use \`balena os initialize <image>\` instead.
|
Use \`balena os initialize <image>\` instead.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
image: Args.string({
|
||||||
name: 'image',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
type: Args.string({
|
||||||
name: 'type',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
config: Args.string({
|
||||||
name: 'config',
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = (
|
public static usage = (
|
||||||
'internal osinit ' +
|
'internal osinit ' +
|
||||||
@ -63,9 +55,10 @@ export default class OsinitCmd extends Command {
|
|||||||
|
|
||||||
public static hidden = true;
|
public static hidden = true;
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<{}, ArgsDef>(OsinitCmd);
|
const { args: params } = await this.parse(OsinitCmd);
|
||||||
|
|
||||||
const config = JSON.parse(params.config);
|
const config = JSON.parse(params.config);
|
||||||
|
|
||||||
|
@ -15,24 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args, Flags } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { applicationIdInfo } from '../utils/messages';
|
import { applicationIdInfo } from '../../utils/messages';
|
||||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
import { parseAsLocalHostnameOrIp } from '../../utils/validation';
|
||||||
import { isV13 } from '../utils/version';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
application?: string;
|
|
||||||
fleet?: string;
|
|
||||||
pollInterval?: number;
|
|
||||||
help?: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
deviceIpOrHostname?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class JoinCmd extends Command {
|
export default class JoinCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -65,21 +53,19 @@ export default class JoinCmd extends Command {
|
|||||||
'$ balena join 192.168.1.25 --fleet MyFleet',
|
'$ balena join 192.168.1.25 --fleet MyFleet',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
deviceIpOrHostname: Args.string({
|
||||||
name: 'deviceIpOrHostname',
|
|
||||||
description: 'the IP or hostname of device',
|
description: 'the IP or hostname of device',
|
||||||
parse: parseAsLocalHostnameOrIp,
|
parse: parseAsLocalHostnameOrIp,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
// Hardcoded to preserve camelcase
|
// Hardcoded to preserve camelcase
|
||||||
public static usage = 'join [deviceIpOrHostname]';
|
public static usage = 'join [deviceIpOrHostname]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
...(isV13() ? {} : { application: cf.application }),
|
|
||||||
fleet: cf.fleet,
|
fleet: cf.fleet,
|
||||||
pollInterval: flags.integer({
|
pollInterval: Flags.integer({
|
||||||
description: 'the interval in minutes to check for updates',
|
description: 'the interval in minutes to check for updates',
|
||||||
char: 'i',
|
char: 'i',
|
||||||
}),
|
}),
|
||||||
@ -90,18 +76,16 @@ export default class JoinCmd extends Command {
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(JoinCmd);
|
||||||
JoinCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const promote = await import('../utils/promote');
|
const promote = await import('../../utils/promote');
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
const logger = await Command.getLogger();
|
const logger = await Command.getLogger();
|
||||||
return promote.join(
|
return promote.join(
|
||||||
logger,
|
logger,
|
||||||
sdk,
|
sdk,
|
||||||
params.deviceIpOrHostname,
|
params.deviceIpOrHostname,
|
||||||
options.application || options.fleet,
|
options.fleet,
|
||||||
options.pollInterval,
|
options.pollInterval,
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -15,21 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KeyAddCmd extends Command {
|
export default class KeyAddCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Add an SSH key to balenaCloud.
|
Add an SSH key to balenaCloud.
|
||||||
@ -60,21 +51,19 @@ export default class KeyAddCmd extends Command {
|
|||||||
'$ balena key add Main %userprofile%.sshid_rsa.pub',
|
'$ balena key add Main %userprofile%.sshid_rsa.pub',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
name: Args.string({
|
||||||
name: 'name',
|
|
||||||
description: 'the SSH key name',
|
description: 'the SSH key name',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
{
|
path: Args.string({
|
||||||
name: `path`,
|
|
||||||
description: `the path to the public key file`,
|
description: `the path to the public key file`,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'key add <name> [path]';
|
public static usage = 'key add <name> [path]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -83,7 +72,7 @@ export default class KeyAddCmd extends Command {
|
|||||||
public static readStdin = true;
|
public static readStdin = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(KeyAddCmd);
|
const { args: params } = await this.parse(KeyAddCmd);
|
||||||
|
|
||||||
let key: string;
|
let key: string;
|
||||||
if (params.path != null) {
|
if (params.path != null) {
|
||||||
|
@ -15,22 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KeyCmd extends Command {
|
export default class KeyCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Display an SSH key.
|
Display an SSH key.
|
||||||
@ -40,25 +30,24 @@ export default class KeyCmd extends Command {
|
|||||||
|
|
||||||
public static examples = ['$ balena key 17'];
|
public static examples = ['$ balena key 17'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
id: Args.integer({
|
||||||
name: 'id',
|
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: (x) => parseAsInteger(x, 'id'),
|
parse: async (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'key <id>';
|
public static usage = 'key <id>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<{}, ArgsDef>(KeyCmd);
|
const { args: params } = await this.parse(KeyCmd);
|
||||||
|
|
||||||
const key = await getBalenaSdk().models.key.get(params.id);
|
const key = await getBalenaSdk().models.key.get(params.id);
|
||||||
|
|
||||||
|
@ -15,23 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsInteger } from '../../utils/validation';
|
import { parseAsInteger } from '../../utils/validation';
|
||||||
|
|
||||||
type IArg<T> = import('@oclif/parser').args.IArg<T>;
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KeyRmCmd extends Command {
|
export default class KeyRmCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Remove an SSH key from balenaCloud.
|
Remove an SSH key from balenaCloud.
|
||||||
@ -43,18 +32,17 @@ export default class KeyRmCmd extends Command {
|
|||||||
|
|
||||||
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
|
public static examples = ['$ balena key rm 17', '$ balena key rm 17 --yes'];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
id: Args.integer({
|
||||||
name: 'id',
|
|
||||||
description: 'balenaCloud ID for the SSH key',
|
description: 'balenaCloud ID for the SSH key',
|
||||||
parse: (x) => parseAsInteger(x, 'id'),
|
parse: async (x) => parseAsInteger(x, 'id'),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'key rm <id>';
|
public static usage = 'key rm <id>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -62,9 +50,7 @@ export default class KeyRmCmd extends Command {
|
|||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(KeyRmCmd);
|
||||||
KeyRmCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const patterns = await import('../../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
|
|
||||||
|
@ -15,14 +15,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import Command from '../../command';
|
||||||
import Command from '../command';
|
import * as cf from '../../utils/common-flags';
|
||||||
import * as cf from '../utils/common-flags';
|
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class KeysCmd extends Command {
|
export default class KeysCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -34,14 +29,14 @@ export default class KeysCmd extends Command {
|
|||||||
|
|
||||||
public static usage = 'keys';
|
public static usage = 'keys';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static authenticated = true;
|
public static authenticated = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
this.parse<FlagsDef, {}>(KeysCmd);
|
await this.parse(KeysCmd);
|
||||||
|
|
||||||
const keys = await getBalenaSdk().models.key.getAll();
|
const keys = await getBalenaSdk().models.key.getAll();
|
||||||
|
|
@ -15,19 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { stripIndent } from '../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
import { parseAsLocalHostnameOrIp } from '../utils/validation';
|
import { parseAsLocalHostnameOrIp } from '../../utils/validation';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help?: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
deviceIpOrHostname?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LeaveCmd extends Command {
|
export default class LeaveCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -51,17 +43,16 @@ export default class LeaveCmd extends Command {
|
|||||||
'$ balena leave 192.168.1.25',
|
'$ balena leave 192.168.1.25',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
deviceIpOrHostname: Args.string({
|
||||||
name: 'deviceIpOrHostname',
|
|
||||||
description: 'the device IP or hostname',
|
description: 'the device IP or hostname',
|
||||||
parse: parseAsLocalHostnameOrIp,
|
parse: parseAsLocalHostnameOrIp,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'leave [deviceIpOrHostname]';
|
public static usage = 'leave [deviceIpOrHostname]';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -69,9 +60,9 @@ export default class LeaveCmd extends Command {
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LeaveCmd);
|
const { args: params } = await this.parse(LeaveCmd);
|
||||||
|
|
||||||
const promote = await import('../utils/promote');
|
const promote = await import('../../utils/promote');
|
||||||
const logger = await Command.getLogger();
|
const logger = await Command.getLogger();
|
||||||
return promote.leave(logger, params.deviceIpOrHostname);
|
return promote.leave(logger, params.deviceIpOrHostname);
|
||||||
}
|
}
|
@ -15,20 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { stripIndent } from '../../utils/lazy';
|
import { stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
target: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LocalConfigureCmd extends Command {
|
export default class LocalConfigureCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
(Re)configure a balenaOS drive or image.
|
(Re)configure a balenaOS drive or image.
|
||||||
@ -41,24 +33,24 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
'$ balena local configure path/to/image.img',
|
'$ balena local configure path/to/image.img',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
target: Args.string({
|
||||||
name: 'target',
|
|
||||||
description: 'path of drive or image to configure',
|
description: 'path of drive or image to configure',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'local configure <target>';
|
public static usage = 'local configure <target>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static root = true;
|
public static root = true;
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params } = this.parse<FlagsDef, ArgsDef>(LocalConfigureCmd);
|
const { args: params } = await this.parse(LocalConfigureCmd);
|
||||||
|
|
||||||
const reconfix = await import('reconfix');
|
const reconfix = await import('reconfix');
|
||||||
const { denyMount, safeUmount } = await import('../../utils/umount');
|
const { denyMount, safeUmount } = await import('../../utils/umount');
|
||||||
@ -96,7 +88,7 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
|
|
||||||
readonly CONNECTIONS_FOLDER = '/system-connections';
|
readonly CONNECTIONS_FOLDER = '/system-connections';
|
||||||
|
|
||||||
getConfigurationSchema(bootPartition: number, connectionFileName?: string) {
|
getConfigurationSchema(bootPartition?: number, connectionFileName?: string) {
|
||||||
connectionFileName ??= 'resin-wifi';
|
connectionFileName ??= 'resin-wifi';
|
||||||
return {
|
return {
|
||||||
mapper: [
|
mapper: [
|
||||||
@ -112,6 +104,12 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
},
|
},
|
||||||
domain: [['config_json', 'hostname']],
|
domain: [['config_json', 'hostname']],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
template: {
|
||||||
|
developmentMode: '{{developmentMode}}',
|
||||||
|
},
|
||||||
|
domain: [['config_json', 'developmentMode']],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
template: {
|
template: {
|
||||||
wifi: {
|
wifi: {
|
||||||
@ -162,6 +160,13 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
name: 'networkKey',
|
name: 'networkKey',
|
||||||
default: data.networkKey,
|
default: data.networkKey,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Enable development mode? (Open ports and root access - Not for production!)',
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'developmentMode',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
message: 'Do you want to set advanced settings?',
|
message: 'Do you want to set advanced settings?',
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
@ -235,9 +240,9 @@ export default class LocalConfigureCmd extends Command {
|
|||||||
async prepareConnectionFile(target: string) {
|
async prepareConnectionFile(target: string) {
|
||||||
const _ = await import('lodash');
|
const _ = await import('lodash');
|
||||||
const imagefs = await import('balena-image-fs');
|
const imagefs = await import('balena-image-fs');
|
||||||
const helpers = await import('../../utils/helpers');
|
const { getBootPartition } = await import('balena-config-json');
|
||||||
|
|
||||||
const bootPartition = await helpers.getBootPartition(target);
|
const bootPartition = await getBootPartition(target);
|
||||||
|
|
||||||
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
const files = await imagefs.interact(target, bootPartition, async (_fs) => {
|
||||||
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
return await promisify(_fs.readdir)(this.CONNECTIONS_FOLDER);
|
||||||
|
@ -15,23 +15,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Args } from '@oclif/core';
|
||||||
import type { BlockDevice } from 'etcher-sdk/build/source-destination';
|
import type { BlockDevice } from 'etcher-sdk/build/source-destination';
|
||||||
import Command from '../../command';
|
import Command from '../../command';
|
||||||
import { ExpectedError } from '../../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
import * as cf from '../../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
|
import { getChalk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
yes: boolean;
|
|
||||||
drive?: string;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class LocalFlashCmd extends Command {
|
export default class LocalFlashCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
Flash an image to a drive.
|
Flash an image to a drive.
|
||||||
@ -49,26 +39,25 @@ export default class LocalFlashCmd extends Command {
|
|||||||
'$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes',
|
'$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
image: Args.string({
|
||||||
name: 'image',
|
|
||||||
description: 'path to OS image',
|
description: 'path to OS image',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'local flash <image>';
|
public static usage = 'local flash <image>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
drive: cf.drive,
|
drive: cf.drive,
|
||||||
yes: cf.yes,
|
yes: cf.yes,
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static offlineCompatible = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(LocalFlashCmd);
|
||||||
LocalFlashCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
const { promisify } = await import('util');
|
const { promisify } = await import('util');
|
||||||
|
@ -15,11 +15,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent, getCliForm } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent, getCliForm } from '../../utils/lazy';
|
||||||
import { ExpectedError } from '../errors';
|
import { ExpectedError } from '../../errors';
|
||||||
|
import type { WhoamiResult } from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
token: boolean;
|
token: boolean;
|
||||||
@ -30,10 +31,7 @@ interface FlagsDef {
|
|||||||
password?: string;
|
password?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
help: void;
|
help: void;
|
||||||
}
|
hideExperimentalWarning: boolean;
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
token?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LoginCmd extends Command {
|
export default class LoginCmd extends Command {
|
||||||
@ -59,37 +57,34 @@ export default class LoginCmd extends Command {
|
|||||||
'$ balena login --credentials --email johndoe@gmail.com --password secret',
|
'$ balena login --credentials --email johndoe@gmail.com --password secret',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args = [
|
public static args = {
|
||||||
{
|
token: Args.string({
|
||||||
// Capitano allowed -t to be type boolean|string, which oclif does not.
|
|
||||||
// So -t is now bool, and we check first arg for token content.
|
|
||||||
name: 'token',
|
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'login';
|
public static usage = 'login';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
web: flags.boolean({
|
web: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
char: 'w',
|
char: 'w',
|
||||||
description: 'web-based login',
|
description: 'web-based login',
|
||||||
exclusive: ['token', 'credentials'],
|
exclusive: ['token', 'credentials'],
|
||||||
}),
|
}),
|
||||||
token: flags.boolean({
|
token: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
char: 't',
|
char: 't',
|
||||||
description: 'session token or API key',
|
description: 'session token or API key',
|
||||||
exclusive: ['web', 'credentials'],
|
exclusive: ['web', 'credentials'],
|
||||||
}),
|
}),
|
||||||
credentials: flags.boolean({
|
credentials: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
char: 'c',
|
char: 'c',
|
||||||
description: 'credential-based login',
|
description: 'credential-based login',
|
||||||
exclusive: ['web', 'token'],
|
exclusive: ['web', 'token'],
|
||||||
}),
|
}),
|
||||||
email: flags.string({
|
email: Flags.string({
|
||||||
char: 'e',
|
char: 'e',
|
||||||
description: 'email',
|
description: 'email',
|
||||||
exclusive: ['user'],
|
exclusive: ['user'],
|
||||||
@ -97,35 +92,38 @@ export default class LoginCmd extends Command {
|
|||||||
}),
|
}),
|
||||||
// Capitano version of this command had a second alias for email, 'u'.
|
// Capitano version of this command had a second alias for email, 'u'.
|
||||||
// Using an oclif hidden flag to support the same behaviour.
|
// Using an oclif hidden flag to support the same behaviour.
|
||||||
user: flags.string({
|
user: Flags.string({
|
||||||
char: 'u',
|
char: 'u',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
exclusive: ['email'],
|
exclusive: ['email'],
|
||||||
dependsOn: ['credentials'],
|
dependsOn: ['credentials'],
|
||||||
}),
|
}),
|
||||||
password: flags.string({
|
password: Flags.string({
|
||||||
char: 'p',
|
char: 'p',
|
||||||
description: 'password',
|
description: 'password',
|
||||||
dependsOn: ['credentials'],
|
dependsOn: ['credentials'],
|
||||||
}),
|
}),
|
||||||
port: flags.integer({
|
port: Flags.integer({
|
||||||
char: 'P',
|
char: 'P',
|
||||||
description:
|
description:
|
||||||
'TCP port number of local HTTP login server (--web auth only)',
|
'TCP port number of local HTTP login server (--web auth only)',
|
||||||
dependsOn: ['web'],
|
dependsOn: ['web'],
|
||||||
}),
|
}),
|
||||||
|
hideExperimentalWarning: Flags.boolean({
|
||||||
|
char: 'H',
|
||||||
|
default: false,
|
||||||
|
description: 'Hides warning for experimental features',
|
||||||
|
}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { flags: options, args: params } = this.parse<FlagsDef, ArgsDef>(
|
const { flags: options, args: params } = await this.parse(LoginCmd);
|
||||||
LoginCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const messages = await import('../utils/messages');
|
const messages = await import('../../utils/messages');
|
||||||
const balenaUrl = await balena.settings.get('balenaUrl');
|
const balenaUrl = await balena.settings.get('balenaUrl');
|
||||||
|
|
||||||
// Consolidate user/email options
|
// Consolidate user/email options
|
||||||
@ -137,9 +135,24 @@ export default class LoginCmd extends Command {
|
|||||||
console.log(`\nLogging in to ${balenaUrl}`);
|
console.log(`\nLogging in to ${balenaUrl}`);
|
||||||
await this.doLogin(options, balenaUrl, params.token);
|
await this.doLogin(options, balenaUrl, params.token);
|
||||||
|
|
||||||
const username = await balena.auth.whoami();
|
// We can safely assume this won't be undefined as doLogin will throw if this call fails
|
||||||
|
// We also don't need to worry too much about the amount of calls to whoami
|
||||||
|
// as these are cached by the SDK
|
||||||
|
const whoamiResult = (await balena.auth.whoami()) as WhoamiResult;
|
||||||
|
|
||||||
console.info(`Successfully logged in as: ${username}`);
|
if (whoamiResult.actorType !== 'user' && !options.hideExperimentalWarning) {
|
||||||
|
console.info(stripIndent`
|
||||||
|
----------------------------------------------------------------------------------------
|
||||||
|
You are logging in with a ${whoamiResult.actorType} key.
|
||||||
|
This is an experimental feature and many features of the CLI might not work as expected.
|
||||||
|
We sure hope you know what you are doing.
|
||||||
|
----------------------------------------------------------------------------------------
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(
|
||||||
|
`Successfully logged in as: ${this.getLoggedInMessage(whoamiResult)}`,
|
||||||
|
);
|
||||||
console.info(`\
|
console.info(`\
|
||||||
|
|
||||||
Find out about the available commands by running:
|
Find out about the available commands by running:
|
||||||
@ -149,6 +162,16 @@ Find out about the available commands by running:
|
|||||||
${messages.reachingOut}`);
|
${messages.reachingOut}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLoggedInMessage(whoami: WhoamiResult): string {
|
||||||
|
if (whoami.actorType === 'user') {
|
||||||
|
return whoami.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identifier =
|
||||||
|
whoami.actorType === 'device' ? whoami.uuid : whoami.slug;
|
||||||
|
return `${whoami.actorType} ${identifier}`;
|
||||||
|
}
|
||||||
|
|
||||||
async doLogin(
|
async doLogin(
|
||||||
loginOptions: FlagsDef,
|
loginOptions: FlagsDef,
|
||||||
balenaUrl: string = 'balena-cloud.com',
|
balenaUrl: string = 'balena-cloud.com',
|
||||||
@ -165,29 +188,36 @@ ${messages.reachingOut}`);
|
|||||||
}
|
}
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
await balena.auth.loginWithToken(token!);
|
await balena.auth.loginWithToken(token!);
|
||||||
if (!(await balena.auth.whoami())) {
|
try {
|
||||||
|
if (!(await balena.auth.whoami())) {
|
||||||
|
throw new ExpectedError('Token authentication failed');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error(`Get user info failed with: ${err.message}`);
|
||||||
|
}
|
||||||
throw new ExpectedError('Token authentication failed');
|
throw new ExpectedError('Token authentication failed');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Credentials
|
// Credentials
|
||||||
else if (loginOptions.credentials) {
|
else if (loginOptions.credentials) {
|
||||||
const patterns = await import('../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
return patterns.authenticate(loginOptions);
|
return patterns.authenticate(loginOptions);
|
||||||
}
|
}
|
||||||
// Web
|
// Web
|
||||||
else if (loginOptions.web) {
|
else if (loginOptions.web) {
|
||||||
const auth = await import('../auth');
|
const auth = await import('../../auth');
|
||||||
await auth.login({ port: loginOptions.port });
|
await auth.login({ port: loginOptions.port });
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
const patterns = await import('../utils/patterns');
|
const patterns = await import('../../utils/patterns');
|
||||||
// User had not selected login preference, prompt interactively
|
// User had not selected login preference, prompt interactively
|
||||||
const loginType = await patterns.askLoginType();
|
const loginType = await patterns.askLoginType();
|
||||||
if (loginType === 'register') {
|
if (loginType === 'register') {
|
||||||
const open = await import('open');
|
const open = await import('open');
|
||||||
const signupUrl = `https://dashboard.${balenaUrl}/signup`;
|
const signupUrl = `https://dashboard.${balenaUrl}/signup`;
|
||||||
open(signupUrl, { wait: false });
|
await open(signupUrl, { wait: false });
|
||||||
throw new ExpectedError(`Please sign up at ${signupUrl}`);
|
throw new ExpectedError(`Please sign up at ${signupUrl}`);
|
||||||
}
|
}
|
||||||
|
|
@ -15,8 +15,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
|
|
||||||
export default class LogoutCmd extends Command {
|
export default class LogoutCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
@ -29,7 +29,7 @@ export default class LogoutCmd extends Command {
|
|||||||
public static usage = 'logout';
|
public static usage = 'logout';
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
this.parse<{}, {}>(LogoutCmd);
|
await this.parse(LogoutCmd);
|
||||||
await getBalenaSdk().auth.logout();
|
await getBalenaSdk().auth.logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -15,24 +15,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { flags } from '@oclif/command';
|
import { Flags, Args } from '@oclif/core';
|
||||||
import Command from '../command';
|
import Command from '../../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||||
import { LogMessage } from 'balena-sdk';
|
import { LogMessage } from 'balena-sdk';
|
||||||
import { IArg } from '@oclif/parser/lib/args';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
|
||||||
'max-retry'?: number;
|
|
||||||
tail?: boolean;
|
|
||||||
service?: string[];
|
|
||||||
system?: boolean;
|
|
||||||
help: void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArgsDef {
|
|
||||||
device: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_RETRY = 1000;
|
const MAX_RETRY = 1000;
|
||||||
|
|
||||||
@ -67,35 +54,34 @@ export default class LogsCmd extends Command {
|
|||||||
'$ balena logs 23c73a1.local --system --service my-service',
|
'$ balena logs 23c73a1.local --system --service my-service',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = {
|
||||||
{
|
device: Args.string({
|
||||||
name: 'device',
|
|
||||||
description: 'device UUID, IP, or .local address',
|
description: 'device UUID, IP, or .local address',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
}),
|
||||||
];
|
};
|
||||||
|
|
||||||
public static usage = 'logs <device>';
|
public static usage = 'logs <device>';
|
||||||
|
|
||||||
public static flags: flags.Input<FlagsDef> = {
|
public static flags = {
|
||||||
'max-retry': flags.integer({
|
'max-retry': Flags.integer({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Maximum number of reconnection attempts on "connection lost" errors
|
Maximum number of reconnection attempts on "connection lost" errors
|
||||||
(use 0 to disable auto reconnection).`,
|
(use 0 to disable auto reconnection).`,
|
||||||
}),
|
}),
|
||||||
tail: flags.boolean({
|
tail: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description: 'continuously stream output',
|
description: 'continuously stream output',
|
||||||
char: 't',
|
char: 't',
|
||||||
}),
|
}),
|
||||||
service: flags.string({
|
service: Flags.string({
|
||||||
description: stripIndent`
|
description: stripIndent`
|
||||||
Reject logs not originating from this service.
|
Reject logs not originating from this service.
|
||||||
This can be used in combination with --system or other --service flags.`,
|
This can be used in combination with --system or other --service flags.`,
|
||||||
char: 's',
|
char: 's',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
}),
|
}),
|
||||||
system: flags.boolean({
|
system: Flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description:
|
description:
|
||||||
'Only show system logs. This can be used in combination with --service.',
|
'Only show system logs. This can be used in combination with --service.',
|
||||||
@ -107,19 +93,17 @@ export default class LogsCmd extends Command {
|
|||||||
public static primary = true;
|
public static primary = true;
|
||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = await this.parse(LogsCmd);
|
||||||
LogsCmd,
|
|
||||||
);
|
|
||||||
|
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
const { serviceIdToName } = await import('../utils/cloud');
|
const { serviceIdToName } = await import('../../utils/cloud');
|
||||||
const { connectAndDisplayDeviceLogs, displayLogObject } = await import(
|
const { connectAndDisplayDeviceLogs, displayLogObject } = await import(
|
||||||
'../utils/device/logs'
|
'../../utils/device/logs'
|
||||||
);
|
);
|
||||||
const { validateIPAddress, validateDotLocalUrl } = await import(
|
const { validateIPAddress, validateDotLocalUrl } = await import(
|
||||||
'../utils/validation'
|
'../../utils/validation'
|
||||||
);
|
);
|
||||||
const Logger = await import('../utils/logger');
|
const Logger = await import('../../utils/logger');
|
||||||
|
|
||||||
const logger = Logger.getLogger();
|
const logger = Logger.getLogger();
|
||||||
|
|
||||||
@ -148,13 +132,13 @@ export default class LogsCmd extends Command {
|
|||||||
validateDotLocalUrl(params.device)
|
validateDotLocalUrl(params.device)
|
||||||
) {
|
) {
|
||||||
// Logs from local device
|
// Logs from local device
|
||||||
const { DeviceAPI } = await import('../utils/device/api');
|
const { DeviceAPI } = await import('../../utils/device/api');
|
||||||
const deviceApi = new DeviceAPI(logger, params.device);
|
const deviceApi = new DeviceAPI(logger, params.device);
|
||||||
logger.logDebug('Checking we can access device');
|
logger.logDebug('Checking we can access device');
|
||||||
try {
|
try {
|
||||||
await deviceApi.ping();
|
await deviceApi.ping();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const { ExpectedError } = await import('../errors');
|
const { ExpectedError } = await import('../../errors');
|
||||||
throw new ExpectedError(
|
throw new ExpectedError(
|
||||||
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
`Cannot access device at address ${params.device}. Device may not be in local mode.`,
|
||||||
);
|
);
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user