Compare commits

...

283 Commits

Author SHA1 Message Date
6606b65c9b v8.0.0 2018-10-19 17:31:41 +02:00
61160fd2f5 Merge pull request #991 from resin-io/v8-meta-branch
Release CLI v8
2018-10-19 17:29:40 +02:00
bf71f9ea16 Merge pull request #981 from resin-io/local-mode-v2
Local mode v2
2018-10-19 17:09:28 +02:00
fe751fdb23 Check supervisor version before attempting to do a local push
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:45:23 +02:00
947f91d570 Support multicontainer local mode in resin push
Change-type: minor
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:44:56 +02:00
c5d4e30e24 logger: Add logs logging function
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:44:53 +02:00
f560aa7523 export resolveProject function from compose module
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:44:49 +02:00
6bcfb2dd51 logs: Add log build function to logger
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:44:44 +02:00
bf062124f7 compose: Add compose typings
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:44:39 +02:00
221666f59a Stop accepting resin-compose.yml as a build composition definition
These files are not supported by any other part of the resin
infrastructure, and it could cause confusion with it not being
supported everywhere. The idea was originally added because we
thought we might need to make extensions on docker-compose, but
that hasn't happened.

Change-type: major
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:49 +02:00
4369a2d161 tconfig: Add skipLibCheck to tsconfig
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:46 +02:00
cd6ee4ef5e Send push source packages as gzipped data
Change-type: minor
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:39 +02:00
872b17cf24 refactor: Allow setting of a remote build error message
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:32 +02:00
88e11347bc tests: Add tests for ignore files
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:28 +02:00
a3dd489c70 Respect ignore files when tarring sources
This commit brings in the ignore and dockerignore libraries, which when
provided with the patterns in the aforementioned files will ignore them.

Change-type: major
Closes: 889
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:43:22 +02:00
0c1c108b2b Check for correct architecture when preloading, instead of correct device type
Preload will now propose to preload any app that matches the image
architecture.

Change-type: major
Signed-off-by: Alexis Svinartchouk <alexis@resin.io>
2018-10-19 16:43:02 +02:00
f02ed43f33 Default preload boolean parameters to false
Change-type: patch
Signed-off-by: Alexis Svinartchouk <alexis@resin.io>
2018-10-19 16:42:51 +02:00
63c3d7ceee fix: Apply prettier to merged files
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:42:48 +02:00
dac45a884e dev: Add fast test npm task, to speed development
Currently running the tests is painfully slow, this commit adds a task
which will run the bare minimum build, and then the tests, speeding up
the process by an order of magnitude.

I had to repeat `gulp test`, instead of reusing `npm run test`, so that
the pretest task isn't ran too.

Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:42:42 +02:00
ec589c2639 Correctly error out on failed remote builds
The push command was relying on the output from the builder to indicate
the build status, but this isn't helpful for CI. This commit makes the
remote build module respect the `isError` flag which the builder sends
in any errors. Any errors which come from the builder indicate the
release will not be deployed.

Change-type: patch
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:42:14 +02:00
f65e777d1b Bump tsconfig target to es6
Change-type: major
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-10-19 16:42:06 +02:00
684ac9fa24 v7.10.9 2018-10-18 21:08:35 +02:00
330cbc6a68 Merge pull request #989 from resin-io/sdk-references-update
Update sdk references in wizzard.coffee
2018-10-18 21:06:38 +02:00
14bfca8c3a v7.10.8 2018-10-18 20:14:43 +02:00
20c07d31b2 Merge pull request #987 from resin-io/sdk-references-update
Update sdk references in device.coffee
2018-10-18 20:12:08 +02:00
64b4f67477 Update sdk references in wizzard.coffee
Change-type:patch
2018-10-18 18:53:03 +02:00
a8ceadc300 v7.10.7 2018-10-18 17:25:57 +02:00
973d25f467 Merge pull request #986 from resin-io/sdk-references-update
Update sdk sdk references in auth.coffee
2018-10-18 17:24:02 +02:00
0d06701e2f Update sdk references in notes.coffee
Change-type:patch
2018-10-18 16:22:35 +02:00
379f1cc217 Update sdk references in device.coffee
Change-type:patch
2018-10-18 16:08:29 +02:00
7b7ae4ff89 Update sdk sdk references in auth.coffee
Change-type:patch
2018-10-18 14:51:03 +02:00
8e83a401eb v7.10.6 2018-10-03 06:58:41 -07:00
2d1891a182 Merge pull request #976 from resin-io/975-fix-preload-examples
Fix formatting of preload examples
2018-10-03 15:56:56 +02:00
8df066df12 Fix formatting of preload examples
Based on https://github.com/resin-io/docs/pull/915 from @drjasonharrison-vp-eio

Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-10-03 15:31:24 +02:00
bd59f95e1a v7.10.5 2018-09-25 07:09:26 -07:00
2b982a1c0c Merge pull request #972 from resin-io/readme-typo
README: Fix typo
2018-09-25 15:08:01 +01:00
ab64fbc904 README: Fix typo
Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2018-09-25 13:35:35 +01:00
733b98f072 v7.10.4 2018-09-24 10:08:55 -07:00
7c538a3658 Merge pull request #967 from resin-io/966-register-print-uuid
device: When registering, print the uuid
2018-09-24 19:07:15 +02:00
8298ba5765 device: When registering, print the uuid
This restores the behavior from before #911,
which is useful from some users.

Closes #966

Change-type: patch
Signed-off-by: Pablo Carranza Velez <pablocarranza@gmail.com>
2018-09-24 15:18:40 +02:00
33a23773d8 v7.10.3 2018-09-19 09:17:52 -07:00
21a3b82845 Merge pull request #971 from resin-io/add-emulated-to-build-docs
Include --emulated in the example resin build parameters
2018-09-19 18:16:28 +02:00
8688eb5da0 Include --emulated in the example resin build parameters
Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-09-19 15:34:29 +02:00
5b0ea9673f v7.10.2 2018-09-18 09:17:50 -07:00
44fd8adeba Merge pull request #970 from resin-io/969-resin-semver
dependencies: Update resin-semver version to support Balena OS
2018-09-18 17:15:09 +01:00
a5e03d55c3 dependencies: Update resin-semver version to support Balena OS
Connects to #969

Change-type: patch
Signed-off-by: Lucian Buzzo <lucian.buzzo@gmail.com>
2018-09-18 14:23:10 +01:00
80629322ea v7.10.1 2018-09-11 05:29:41 -07:00
946efbcb7f Merge pull request #965 from resin-io/964-drop-npm-deploy
Stop Travis deploying to npm (now handled by concourse)
2018-09-11 14:28:11 +02:00
be8a314d2b Stop Travis deploying to npm (now handled by concourse)
Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-09-11 13:58:06 +02:00
0a7203cafe v7.10.0 2018-09-11 04:21:19 -07:00
786fed0151 Merge pull request #963 from resin-io/update-resin-cli-form
Update resin-cli-form to 2.x
2018-09-11 12:17:22 +01:00
9cd8228a20 Update resin-cli-form to 2.x
Change-type: minor
Signed-off-by: Pagan Gazzard <page@resin.io>
2018-09-10 18:31:51 +01:00
652b5f22dd v7.9.4 2018-09-10 06:34:48 -07:00
eed3c06789 Merge pull request #911 from resin-io/fix_pre_provision
Device api keys are no longer used in the registration process
2018-09-10 15:33:37 +02:00
3b283d4a98 Device api keys are no longer used in the registration process
Change-type: patch
Signed-off-by: Theodor Gherzan <theodor@resin.io>
2018-09-10 12:30:51 +01:00
bc6b5ba7b3 Auto-merge for PR #952 via VersionBot
Fix configuration hangs with some images using a larger threadpool
2018-08-20 15:37:22 +00:00
74789ae88f v7.9.3 2018-08-20 15:29:02 +00:00
295d6dee74 Fix configuration hangs with some images by expanding the threadpool
Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-20 17:06:26 +02:00
5010a1e312 Auto-merge for PR #946 via VersionBot
Add warning about re-enabling automatic updates
2018-08-15 21:39:23 +00:00
3c2f7ea622 v7.9.2 2018-08-15 21:31:24 +00:00
94f02f0ad8 Add warning about re-enabling automatic updates
Change-type: patch
Signed-off-by: Pagan Gazzard <page@resin.io>
2018-08-15 14:20:11 -07:00
375f84b24e Auto-merge for PR #942 via VersionBot
Fix errors in `getRequestStream` not being propogated
2018-08-15 18:08:59 +00:00
06c649dfd0 v7.9.1 2018-08-15 17:59:46 +00:00
71eca70a22 Fix errors in getRequestStream not being propogated
Change-type: patch
Signed-off-by: Pagan Gazzard <page@resin.io>
2018-08-14 18:21:10 -07:00
53c7bc622c Auto-merge for PR #903 via VersionBot
Support emulated and nocache options for remote builds
2018-08-09 14:52:03 +00:00
975ae45e49 v7.9.0 2018-08-09 14:42:30 +00:00
e7c68c1a5c Support emulated and nocache options for remote builds
Change-type: minor
Closes: #901
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-08-09 14:36:34 +01:00
5beeb78220 Auto-merge for PR #939 via VersionBot
Fix bug where the sudo helper failed in os initialize
2018-08-09 10:37:53 +00:00
c90ba7aa0f v7.8.6 2018-08-09 10:29:50 +00:00
802ccc1b9a Fix bug where the sudo helper failed in os initialize
Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-09 12:11:26 +02:00
b6ef251625 Auto-merge for PR #934 via VersionBot
Add an env vars example config to the local push docs
2018-08-09 10:09:37 +00:00
fd707d6a07 v7.8.5 2018-08-09 10:01:55 +00:00
392cd8569f Make build trigger hash examples clearer
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-09 11:47:21 +02:00
e32eda26d9 Update .resin-sync.yml docs for local push and include example env vars
Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-09 11:21:45 +02:00
d8aaccf80c Update typed-error to fix some TS complaints
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-09 11:21:45 +02:00
d5fd5f5f2d Auto-merge for PR #936 via VersionBot
Update klaw now that the fork changes has been finished & released
2018-08-02 10:37:40 +00:00
2cb69c12f1 v7.8.4 2018-08-02 10:29:33 +00:00
7c75346a1a Update klaw
The changes from our fork have now been completed and released

Change-type: patch
Signed-off-by: Tim Perry <tim@resin.io>
2018-08-01 16:43:26 +02:00
148d15b6d9 Auto-merge for PR #931 via VersionBot
Follow links found during builds
2018-07-25 14:07:19 +00:00
a46a79df59 v7.8.3 2018-07-25 13:58:28 +00:00
e350f9b335 Follow links found during builds
Change-Type: patch
2018-07-25 12:38:17 +02:00
bd00773f1b Auto-merge for PR #929 via VersionBot
Update reconfix to fix volume signature errors in local configure
2018-07-25 10:23:37 +00:00
ef3c7f0fd6 v7.8.2 2018-07-25 10:13:48 +00:00
f4f44f978e Update reconfix to fix volume signature errors in local configure
Change-Type: patch
2018-07-24 20:57:40 +02:00
442416efc3 Auto-merge for PR #930 via VersionBot
Be explicit about how much initial history log tailing includes
2018-07-20 18:07:00 +00:00
ef33ffedcf v7.8.1 2018-07-20 17:38:09 +00:00
430d4aeaa7 Be explicit about how much initial history log tailing includes
Change-Type: patch
2018-07-20 16:32:31 +02:00
171632f83f Auto-merge for PR #895 via VersionBot
Add join/leave commands to promote and move devices between platforms
2018-07-20 12:36:20 +00:00
1fa7141b58 v7.8.0 2018-07-20 10:40:22 +00:00
916cc36430 Lazily import resin-image-fs
If for whatever reason resin-image-fs is not importable — eg. if it’s built for another arch — any command that imports `helpers.ts` will just quit without any error/traceback.
2018-07-20 13:04:26 +03:00
27b877dd33 Forward root CA to device config if one is present 2018-07-19 22:34:31 +03:00
5cbe1c410f Add join/leave commands to promote and move devices between platforms
Both commands work with local devices by remotely invoking the `os-config` executable via SSH. This requires an as of yet unreleased resinOS (that will most likely be v2.14) and the commands ascertain compatibility merely by looking for the `os-config` executable in the device, and bail out if it’s not present.

`join` and `leave` accept a couple of optional arguments and implement a wizard-style interface if these are not given. They allow to interactively select the device and the application to promote to. If the user has no apps, `join` will offer the user to create one. `join` will also offer the user to login or create an account if they’re not logged in already without exiting the wizard.

`resin-sync` (that's used internally to discover local devices) requires admin privileges. If no device has been specified as an argument, the commands will launch the device scanning process in a privileged subprocess via two new internal commands: `internal sudo` and `internal scanDevices`. This avoids having the user to invoke the commands with sudo and only request escalation if truly needed. This commit also removes the dependency to “president”, implementing “sudo” functionality within the CLI.

Change-Type: minor
2018-07-19 22:18:02 +03:00
7846af390e Improve selectFromList function signature to be much more reusable 2018-07-19 21:53:43 +03:00
79d9ebc805 Auto-merge for PR #923 via VersionBot
Update OS & config actions to the MC SDK, and add a --version option
2018-07-17 15:43:30 +00:00
25b853c535 v7.7.4 2018-07-17 15:35:26 +00:00
a93141343f Update TypeScript to 2.8.1
Change-Type: patch
2018-07-17 16:48:14 +02:00
9a467c5ecd Pin all type modules 2018-07-17 15:59:31 +02:00
70be2ae596 Tweaks to config options handling after review 2018-07-17 15:38:38 +02:00
36eb0a108e Post-review tweaks to OS actions 2018-07-13 19:34:59 +02:00
0bf6fb1739 Add --version options to os configure & config generate
This is used to ensure the correct type of API key is used in all
configuration.

Change-Type: patch
2018-07-13 19:34:59 +02:00
892adf4c47 Update OS & config actions to the latest SDK
Fixes #915
Change-Type: patch
2018-07-13 19:34:59 +02:00
5d1d004b72 Auto-merge for PR #927 via VersionBot
Update the CLI deploy key since npm invalidated the old one
2018-07-13 17:21:18 +00:00
dea5a60b2d v7.7.3 2018-07-13 17:05:32 +00:00
652a1b7650 Update the deploy key since npm invalidated the old one
Change-Type: patch
2018-07-13 16:39:56 +02:00
350843af1e Auto-merge for PR #926 via VersionBot
Pin ext2fs to 1.0.7 to avoid temporary deployment issues
2018-07-13 11:40:25 +00:00
e04c4a8ee3 v7.7.2 2018-07-13 11:33:13 +00:00
9d0c3f7535 Pin ext2fs to 1.0.7 to avoid temporary deployment issues
Change-Type: patch
2018-07-13 13:20:53 +02:00
9561d4da2e Auto-merge for PR #925 via VersionBot
Update logs to use new v10 MC SDK
2018-07-12 13:59:28 +00:00
8296dcf946 v7.7.1 2018-07-12 13:52:10 +00:00
e62e8b88c2 Simplify logs promises after review 2018-07-12 15:38:27 +02:00
4388a248b9 Make sure we don't duplicate historical logs when streaming 2018-07-12 15:23:33 +02:00
f9cf0aaf23 Remove a couple of artifacts of the pubnub logs implementation 2018-07-12 15:10:16 +02:00
dc9ee09838 Update CLI to SDK v10 (include new API logs)
Change-Type: patch
2018-07-12 01:03:16 +02:00
7cb27283c5 Update logs action to use the MC SDK 2018-07-12 01:03:16 +02:00
10a9840b34 Auto-merge for PR #921 via VersionBot
Add --generate-device-api-key parameter to config generate
2018-07-11 04:28:25 +00:00
ce3e04bfe8 v7.7.0 2018-07-11 04:21:42 +00:00
52f93f8f12 Add --generate-device-api-key parameter to config generate
Change-Type: minor
2018-07-10 19:57:56 +02:00
af9e1a122d Auto-merge for PR #910 via VersionBot
Make local commands more resilient to unnamed containers
2018-06-28 16:26:11 +00:00
9017b8ec11 v7.6.2 2018-06-28 12:55:34 +00:00
bf4f687a2a Make local commands more resilient to unnamed containers
Change-Type: patch
2018-06-28 12:34:31 +02:00
9d4e6eb825 Auto-merge for PR #907 via VersionBot
Make sure 'resin push' is included in the CLI docs
2018-06-26 17:22:44 +00:00
fba4afb7d2 v7.6.1 2018-06-26 17:15:20 +00:00
8c74f784f7 Make sure 'resin push' is included in the docs
Fixes #906
Change-Type: patch
2018-06-26 19:00:20 +02:00
69ca1ffa59 Auto-merge for PR #896 via VersionBot
Support pinned release preloading
2018-06-20 17:00:12 +00:00
7d1b00877e v7.6.0 2018-06-20 16:50:01 +00:00
1a48fed1f7 Support pinned release preloading
Change-type: minor
Closes: #886
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-06-13 12:29:30 +01:00
bc86359e63 Auto-merge for PR #893 via VersionBot
Document Python native build dependency
2018-06-12 18:33:47 +00:00
f6822f1502 v7.5.2 2018-06-12 18:26:12 +00:00
398c34d842 Includes new prettier changes, and pin prettier to stop more appearing 2018-06-12 17:43:15 +02:00
72a893be95 Document Pyhton native build dependency
Change-Type: patch
2018-06-12 17:11:45 +02:00
7b23b0e103 Auto-merge for PR #887 via VersionBot
Add a multicontainer caveat to the env var commands
2018-06-01 11:10:33 +00:00
0ce7878042 v7.5.1 2018-06-01 10:49:15 +00:00
da8483e6a6 Add a multicontainer caveat to the env var commands
Change-Type: patch
2018-06-01 12:37:29 +02:00
16f70fd946 Auto-merge for PR #883 via VersionBot
Update resin-compose-parse dependency version
2018-05-31 16:16:47 +00:00
78aa898b37 v7.5.0 2018-05-31 16:07:38 +00:00
b7f94a222d Update resin-compose-parse dependency version to 1.10.2
Change-type: minor
2018-05-30 11:57:04 -03:00
7bea2c26b8 Auto-merge for PR #879 via VersionBot
Update SDK for device commands, so we show new device dashboard URLs
2018-05-24 14:13:01 +00:00
7c178b8095 v7.4.1 2018-05-24 14:03:02 +00:00
865f085094 Make sure we still show the device commit, despite API changes 2018-05-24 14:43:45 +01:00
28fe69fe94 Update to latest SDK in lots of easy device commands 2018-05-18 20:05:24 +02:00
232cf8d426 Update SDK in resin device(s) to ensure the dashboard URL is correct
Fixes #768

Change-Type: patch
2018-05-18 20:00:40 +02:00
22e74983b0 Auto-merge for PR #868 via VersionBot
Add push command which starts a build on remote resin servers
2018-05-10 12:44:43 +00:00
c88dd2257a v7.4.0 2018-05-10 12:28:32 +00:00
439d8d396f Add push command which starts a build on remote resin servers
Change-type: minor
Connects-to: #843
2018-05-10 11:43:45 +01:00
6d8086c09b Auto-merge for PR #874 via VersionBot
Handle failed requires & missing bindings
2018-05-03 17:56:53 +00:00
e85f252f29 v7.3.8 2018-05-03 17:49:06 +00:00
4b818ad51c Style improvements after review 2018-05-03 18:59:28 +02:00
c2518448a3 Catch require errors and provide helpful instructions
Change-Type: patch
2018-05-03 16:01:40 +02:00
e7a8deed05 Inline the entire resin-cli-errors module
It's awkward that error handling requires you to go to a different
package, it makes things more complicated, and there's nowhere else that
really should be reusing this logic. Let's inline it, so we can
deprecate the module entirely.

Change-Type: patch
2018-05-03 15:15:03 +02:00
0ac599d20c Auto-merge for PR #871 via VersionBot
Pin node types to v9.0.0 to avoid build errors with transient dependencies
2018-04-30 15:25:23 +00:00
7d7074e6b7 v7.3.7 2018-04-30 15:18:31 +00:00
35ca34d07d Pin node types to v9.0.0 to avoid build errors with transient dependencies
Change-type: patch
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-04-30 16:09:12 +01:00
90d7316b4c Auto-merge for PR #870 via VersionBot
Update resin-image-fs to stop non-config commands failing in node 10
2018-04-30 09:54:09 +00:00
904b4e96d9 v7.3.6 2018-04-30 09:34:40 +00:00
2c46c59a79 Update resin-image-fs to stop non-config commands failing in node 10
This doesn't fix actual usage of image fs, just makes it possible to
stop commands that don't use it from failing entirely.

Connects-To: #869
Change-Type: patch
2018-04-30 11:14:39 +02:00
297ff86895 Auto-merge for PR #858 via VersionBot
Don't show Docker container status from devices, as it can be wrong
2018-04-18 19:08:16 +00:00
a154401424 v7.3.5 2018-04-18 19:00:21 +00:00
ad2713fc00 Don't show Docker container status from devices, as it can be wrong
The status includes a description of how long the device has been in
this state (Up 6 weeks), which is frequently wrong as when the device
first starts up its clock isn't up to date. It's confusing and messy,
best to just remove it entirely.

Fixes #828
Change-Type: patch
2018-04-18 20:16:44 +02:00
6388cfaf40 Auto-merge for PR #865 via VersionBot
Include resin compose schemas in the standalone build
2018-04-18 16:41:50 +00:00
167f38e342 v7.3.4 2018-04-18 16:27:52 +00:00
919b3c3435 Include resin compose schemas in the standalone build
Fixes #844
Change-Type: patch
2018-04-18 13:34:35 +02:00
2e1ab22173 Auto-merge for PR #861 via VersionBot
727 sentry improvements
2018-04-17 14:46:13 +00:00
0a23563d7e v7.3.3 2018-04-17 14:01:51 +00:00
37e4ec6364 Rename expectedError to exitWithExpectedError 2018-04-17 15:18:06 +02:00
6a8b947c2e Don't report lots of user input errors
Change-Type: patch
2018-04-17 15:18:06 +02:00
a16ac37625 Include Sentry breadcrumbs for context in error reports
Change-Type: patch
2018-04-17 15:18:06 +02:00
cf4c7826b2 Update to Sentry 2.x
Change-Type: patch
2018-04-17 15:18:06 +02:00
a0a26f0a1e Auto-merge for PR #862 via VersionBot
Update Dockerode to fix local push issue in standalone builds
2018-04-16 16:21:22 +00:00
a921139a12 v7.3.2 2018-04-16 15:21:33 +00:00
36da7b66c8 Update Dockerode to fix local push issue in standalone builds
Connects-To: #824
Change-Type: patch
2018-04-16 16:43:17 +02:00
3aa87544eb Auto-merge for PR #849 via VersionBot
Update resin-compose-parse to v1.8.1 to fix a problem parsing ports
2018-04-13 19:43:58 +00:00
6121fa505e v7.3.1 2018-04-13 19:38:01 +00:00
a5ba5befd1 Update resin-compose-parse to v1.8.1 to fix a problem parsing ports
Connects-to: https://github.com/resin-io/resin-supervisor/issues/618

Change-Type: patch
Signed-off-by: Pablo Carranza Velez <pablo@resin.io>
2018-04-13 11:17:18 -07:00
b7214a306c Auto-merge for PR #854 via VersionBot
Add 'api-key generate' command
2018-04-12 10:24:39 +00:00
d7616e941a v7.3.0 2018-04-12 10:06:09 +00:00
834a2f1e4d Warn user that api keys will not be shown again in future 2018-04-11 19:31:03 +02:00
0e5f2fe748 Remove now-unused stream-to-promise dependency 2018-04-11 19:30:29 +02:00
e0bcb5e0b9 Always call done() for api key generation, not just if we're successful 2018-04-11 19:27:58 +02:00
59d4890eae Add 'api-key generate' command
Change-Type: minor
2018-04-10 19:21:37 +02:00
51da5360da Auto-merge for PR #852 via VersionBot
Explicitly depend on tar-stream
2018-04-10 14:14:38 +00:00
2655aef28b v7.2.4 2018-04-10 13:49:09 +00:00
45d3a7a124 Explicitly depend on tar-stream
Change-Type: patch
2018-04-10 13:10:25 +02:00
662e4f8940 Merge pull request #853 from resin-io/document-version-rec
Correct documented node version requirement to 6+
2018-04-10 13:10:02 +02:00
c06993cb8e Correct documented node version requirement to 6+
Change-Type: patch
2018-04-09 16:55:36 +02:00
a650f30ce8 Auto-merge for PR #847 via VersionBot
Add a fast build script to package.json
2018-04-06 17:11:31 +00:00
0a924b2dcb v7.2.3 2018-04-06 16:27:32 +00:00
89f62683ce Add a fast build script to package.json
This doesn't run a linter or any documentation generation, aiding in
quick development time.

Change-type: patch
Signed-off-by: Cameron Diver <cameron@resin.io>
2018-04-06 15:40:08 +01:00
143d88f3df Auto-merge for PR #846 via VersionBot
Throw a clear error when logging in with an invalid token
2018-04-04 19:34:56 +00:00
d166a65422 v7.2.2 2018-04-04 18:56:26 +00:00
dd268993b3 Throw a clear error when logging in with an invalid token
Change-Type: patch
2018-04-04 15:43:34 +02:00
13a35b288f Auto-merge for PR #839 via VersionBot
Update docker-qemu-transpose to avoid the broken 0.4.1 release
2018-03-29 14:47:30 +00:00
81e653d31b v7.2.1 2018-03-29 13:52:06 +00:00
875ec8b8bd Update docker-qemu-transpose to avoid the broken 0.4.1 release
Change-Type: patch
2018-03-29 15:28:56 +02:00
989df9b857 Auto-merge for PR #835 via VersionBot
Initial support for api keys in the CLI
2018-03-29 10:15:31 +00:00
0829d3c176 v7.2.0 2018-03-29 10:09:08 +00:00
ce64889b04 Clarify isTokenValid logic 2018-03-29 11:11:25 +02:00
d3a0bfc5f6 Fix auth utils tests to work with new SDK 2018-03-29 11:11:25 +02:00
e965c603d2 Use spec test reporter, so we can debug with output 2018-03-29 11:11:25 +02:00
0e2fb8c96c Promisify auth utils tests 2018-03-29 11:11:25 +02:00
2db1d84d3c Do not require a login for builds
Fixes: #578
Change-Type: patch
2018-03-29 11:11:25 +02:00
12a1916007 Allow (experimental!) login with API keys
Change-Type: minor
2018-03-29 11:11:25 +02:00
b4526e9895 Auto-merge for PR #838 via VersionBot
Fix build emulation for multi-stage builds
2018-03-29 09:03:40 +00:00
a2d867c860 v7.1.6 2018-03-29 08:56:07 +00:00
05b1c37379 Fix build emulation for multi-stage builds
Fixes #814
Change-Type: patch
2018-03-29 10:18:31 +02:00
906cfe9268 Auto-merge for PR #834 via VersionBot
Fix crash when an app is not specified for build command
2018-03-28 12:01:10 +00:00
3c8054faa7 v7.1.5 2018-03-27 17:51:36 +00:00
c6c9046826 Fix crash when an app is not specified for build command
This is a regression introduced in #818

Change-Type: patch
2018-03-27 19:12:31 +03:00
2bbbbf6fdd Auto-merge for PR #832 via VersionBot
Upgrade resin-sync to pull in the fix for #824
2018-03-26 16:31:46 +00:00
9cce4001af v7.1.4 2018-03-26 16:09:22 +00:00
2e944cf2f4 Upgrade resin-sync to pull in the fix for #824
Change-Type: patch
2018-03-26 17:39:47 +02:00
2b0143775c Auto-merge for PR #831 via VersionBot
Prefix all pine options with '$' in preload to avoid pine warnings.
2018-03-23 15:57:54 +00:00
49fec7d8f2 v7.1.3 2018-03-23 15:49:16 +00:00
ca1ac2bb83 Prefix all pine options with '$' in preload to avoid pine warnings.
Change-Type: patch
2018-03-23 15:20:18 +00:00
50b1a7e6b0 Auto-merge for PR #830 via VersionBot
Update resin-preload to 6.2.0 and resin-sdk to 9.0.0-beta16
2018-03-23 13:56:14 +00:00
69ce2c0473 v7.1.2 2018-03-23 13:49:24 +00:00
a3b446dbe7 Update resin-preload to 6.2.0 and resin-sdk to 9.0.0-beta16
Change-Type: patch
2018-03-23 13:41:16 +00:00
1032d9927f Auto-merge for PR #827 via VersionBot
Remove explicit anchor links in CLI docs
2018-03-22 18:03:07 +00:00
12e8a50abc v7.1.1 2018-03-22 17:06:07 +00:00
a4142097f8 Merge branch 'master' into doc-headings 2018-03-22 09:17:32 -05:00
b388ccb6f3 Auto-merge for PR #818 via VersionBot
Restore legacy deployment method
2018-03-22 11:43:59 +00:00
e011502b7e v7.1.0 2018-03-22 11:36:41 +00:00
4f167cb836 Address review feedback 2018-03-22 13:26:47 +02:00
9455d438e2 Formatting fixes 2018-03-22 13:26:47 +02:00
a356ecf9b6 Remove unused code 2018-03-22 13:26:47 +02:00
066ac591ac Warn early if deploying a multicontainer project to an incompatible app
Change-Type: patch
2018-03-22 13:26:47 +02:00
62f006b89a Add legacy deploy method back
This mostly reverts the removal of the legacy deploy code that pushed image tars via the builder. It’s needed for users to avoid having to switch between CLI versions in order to push to legacy apps as well.

Note: this pins resin-sdk to 9.0.0-beta14 as I couldn’t get it to install otherwise — npm would always install 9.0.0-beta9 instead.

Change-Type: minor
2018-03-22 13:26:47 +02:00
ee75ff2753 Remove explicit anchor links in CLI docs
Our docs markdown renderer doesn't process explicit anchor tags, as it generates its own. The script that generates the markdown has been updated to not include these tags and to properly build the TOC links.

Change-type: patch
2018-03-20 13:10:07 -05:00
e4c9defb70 Auto-merge for PR #821 via VersionBot
Update resin-preload to 6.1.2
2018-03-20 15:54:28 +00:00
bb102c1918 v7.0.7 2018-03-20 15:44:13 +00:00
24ebe2946c Update resin-preload to 6.1.2
Connects-To: #820

Change-Type: patch
2018-03-20 15:22:59 +00:00
ba82b1fa27 Auto-merge for PR #815 via VersionBot
Build/deploy commands improvements
2018-03-20 10:43:31 +00:00
e3b145e7b7 v7.0.6 2018-03-20 10:33:01 +00:00
242c3731ee Remove redundant import 2018-03-19 20:52:51 +02:00
5f7eee8eac Make sure image name is all lowercase
Change-Type: patch
2018-03-19 20:52:51 +02:00
1833f6ff0a Improve handling of build log output
This makes sure build logs don’t leak escape sequences and new lines and they don’t break the output. Also improved “inline” logs by normalising the stream before passing it to “transpose build stream”.

Fixes: #808
Change-Type: patch
2018-03-19 20:52:51 +02:00
e5fb954645 Auto-merge for PR #801 via VersionBot
add bash completions
2018-03-15 20:03:33 +00:00
13f76dc020 v7.0.5 2018-03-15 18:51:46 +00:00
b409bdcc73 add blurb about bash completion
Add brief information about tab completions for bash and instructions to enable it.
2018-03-15 18:06:04 +01:00
8c3cb3f585 Add bash completions
This contains bash completion functionality for the resin CLI, including completion for sub-commands.

Change-type: patch
2018-03-15 18:05:50 +01:00
76a8b4df50 Auto-merge for PR #813 via VersionBot
Properly generate consistent working anchors for both our md output & resin docs
2018-03-15 12:09:16 +00:00
a03680311d v7.0.4 2018-03-15 12:01:05 +00:00
6ee36cb5c7 Generate consistent working anchors for both our md output & resin docs
Change-Type: patch
2018-03-15 11:40:29 +01:00
5625326c65 Auto-merge for PR #812 via VersionBot
Fix getting window size when there’s no TTY attached
2018-03-15 08:54:19 +00:00
b912419839 v7.0.3 2018-03-15 08:47:28 +00:00
fe01ead023 Fix getting window size when there’s no TTY attached
Change-Type: patch
2018-03-15 10:30:54 +02:00
229c105d0c Auto-merge for PR #807 via VersionBot
Update full CLI docs with recent installation improvements too
2018-03-13 12:00:31 +00:00
b6e044345f v7.0.2 2018-03-13 10:47:55 +00:00
d9906121e1 Update full CLI docs with recent installation improvements too
Change-Type: patch
2018-03-12 22:17:20 +01:00
3e019f7f34 Remove leftover capitanodoc.coffee file (it's now TS) 2018-03-12 20:06:44 +01:00
eb34cb6f27 Auto-merge for PR #805 via VersionBot
Recommend unsafe-perm to fix some install issues and cleanup dependencies after MC
2018-03-12 16:36:28 +00:00
3a3178bcb9 v7.0.1 2018-03-12 15:36:10 +00:00
cdf6580ecc Recommend using unsafe-prem to avoid permission issues on install
Change-Type: patch
2018-03-12 13:36:24 +01:00
c42bc74f1f Remove unnecessary resin-cli-auth dependency
Change-Type: patch
2018-03-12 11:41:58 +01:00
35fd79f577 Remove (duplicated) runtime ts-node dependency 2018-03-12 11:41:14 +01:00
4ef0682e5a Auto-merge for PR #792 via VersionBot
Multicontainer
2018-03-09 22:12:00 +00:00
d0b7047189 v7.0.0 2018-03-09 22:04:51 +00:00
ae3f936b66 Update resin-preload to v6.0.0 2018-03-09 21:53:34 +00:00
1ef492809b Update resin-preload to v6.0.0-beta11 2018-03-09 20:40:13 +00:00
5bf9dd3a9d Update resin-preload to v6.0.0-beta10 2018-03-09 17:50:20 +00:00
b18a66f66b Update resin-preload to v6.0.0-beta9 2018-03-09 17:02:44 +00:00
1dadfdc699 Fix some formatting to make prettier+resin-lint happy 2018-03-07 16:16:07 +01:00
14a3f51b73 Add docker-compose-aware builds and deployments
Legacy behaviour is mostly retained. The most notable change in behaviour is that invoking `resin deploy` without options is now allowed (see help string how it behaves).

In this commit there are also the following notable changes:

- Deploy/Build are promoted to primary commands
- Extracts QEMU-related code to a new file
- Adds a utility file to retrieve the CLI version and its parts
- Adds a helper that can be used to manipulate display on capable clients
- Declares several new dependencies. Most are already indirectly installed via some dependency

Change-Type: minor
2018-03-07 14:48:05 +00:00
96116aeaec Fix invoking undefined method
Have no idea how this used to work.
2018-03-07 14:47:16 +00:00
7fd31b6a64 Update YAML parser
New version is 3.10.0
2018-03-07 14:47:16 +00:00
299bc0db13 Update docker-toolbelt
New version is 3.1.0.

The updated version is not backwards compatible as it removes all *Async methods that are in wide use in the CLI. The workaround for now is to manually promisify the client and replace all `new Docker()` calls with a shared function that returns a promisified client.
2018-03-07 14:47:15 +00:00
4b9ccae442 Update bundle-resolve and docker-build to latest
This brings in maintainance improvements.

New versions are:

- resin-bundle-resolve: 0.5.1
- resin-docker-build: 0.6.2
2018-03-07 14:46:35 +00:00
079ce552e3 *BREAKING*: Remove support for plugins entirely
There are very few plugins in real-world use, we're not actively working
on this at all, and the current approach won't work once we move to
standalone node-less binary installation anyway.

Change-Type: major
2018-03-07 14:46:35 +00:00
163684e3a9 Update dashboard login to use the multicontainer SDK
Change-Type: patch
2018-03-07 14:46:35 +00:00
f698f561c9 Multicontainer preload: Update resin-preload to 6.0.0-beta4
Change-Type: minor
2018-03-07 14:46:35 +00:00
cb207f18a5 Update the keys action to use the multicontainer SDK
Change-Type: patch
2018-03-07 14:46:34 +00:00
76a5cdc977 Require multicontainer SDK
* require('resin-sdk') => multicontainer SDK
 * require('resin-sdk-preconfigured') => 6.15.0 SDK
 * all 'resin-sdk' requires replaced with 'resin-sdk-preconfigured'
 * resin-sdk-preconfigured TS typings are copy pasted from the current resin-sdk master

The idea is to progressively replace all 'resin-sdk-preconfigured'
requires with 'resin-sdk' (multicontainer sdk) and eventually remove
resin-sdk-preconfigured from package.json.

Change-Type: patch
2018-03-07 14:46:31 +00:00
a82af1d2d1 Auto-merge for PR #802 via VersionBot
Fix CLI prettier configuration to avoid linting errors
2018-03-07 14:46:08 +00:00
ac7d51ad80 v6.13.5 2018-03-07 14:38:49 +00:00
797a739c92 Fix prettier configuration to avoid linting errors
Change-Type: patch
2018-03-05 16:02:09 +01:00
666b59b463 Auto-merge for PR #796 via VersionBot
Fix issue where emulated builds broke Docker `ENV` commands
2018-02-22 18:30:14 +00:00
a83d9a070c v6.13.4 2018-02-22 18:23:29 +00:00
7637377471 Fix issue where emulated builds broke Docker ENV commands
Connects-to: #795
Change-type: patch
2018-02-22 18:12:17 +00:00
6515f88d92 Auto-merge for PR #793 via VersionBot
Tweak TS & add missing deps that may cause build failures in some envs
2018-02-20 22:07:21 +00:00
92534b9c82 v6.13.3 2018-02-20 21:30:39 +00:00
c12360daa8 Tweak TS & add missing deps that may cause build failures in some envs
Connects-To: #765
Change-Type: patch
2018-02-20 20:26:18 +01:00
3d28118f3e Auto-merge for PR #794 via VersionBot
Ensure login does not wait for the browser process to close
2018-02-20 19:00:32 +00:00
04adfde064 v6.13.2 2018-02-20 17:12:45 +00:00
d8aabfd448 Ensure login does not wait for the browser process to close
Unclear why, but for some reason this only actually blocked on the
browser on OSX.

Connects-To: #791
Change-Type: patch
2018-02-16 17:28:19 +01:00
75 changed files with 5536 additions and 1156 deletions

View File

@ -1,5 +1,5 @@
{
"single-quote": true,
"trailing-comma": "all",
"use-tabs": true
"singleQuote": true,
"trailingComma": "all",
"useTabs": true
}

View File

@ -17,12 +17,3 @@ deploy:
tags: true
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
repo: resin-io/resin-cli
- provider: npm
email: accounts@resin.io
api_key:
secure: phet6Du13hc1bzStbmpwy2ODNL5BFwjAmnpJ5wMcbWfI7fl0OtQ61s2+vW5hJAvm9fiRLOfiGAEiqOOtoupShZ1X8BNkC708d8+V+iZMoFh3+j6wAEz+N1sVq471PywlOuLAscOcqQNp92giCVt+4VPx2WQYh06nLsunvysGmUM=
skip_cleanup: true
on:
tags: true
condition: "$TRAVIS_TAG =~ ^v?[[:digit:]]+\\.[[:digit:]]+\\.[[:digit:]]+"
repo: resin-io/resin-cli

View File

@ -4,6 +4,305 @@ All notable changes to this project will be documented in this file
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
This project adheres to [Semantic Versioning](http://semver.org/).
## v8.0.0 - 2018-10-19
* Support multicontainer local mode in resin push [Cameron Diver]
* Stop accepting resin-compose.yml as a build composition definition [Cameron Diver]
* Send push source packages as gzipped data [Cameron Diver]
* Respect ignore files when tarring sources [Cameron Diver]
* Check for correct architecture when preloading, instead of correct device type [Alexis Svinartchouk]
* Default preload boolean parameters to false [Alexis Svinartchouk]
* Correctly error out on failed remote builds [Cameron Diver]
* Bump tsconfig target to es6 [Cameron Diver]
## v7.10.9 - 2018-10-18
* Update sdk references in wizzard.coffee [Scott Lowe]
## v7.10.8 - 2018-10-18
* Update sdk references in notes.coffee [Scott Lowe]
* Update sdk references in device.coffee [Scott Lowe]
## v7.10.7 - 2018-10-18
* Update sdk sdk references in auth.coffee [Scott Lowe]
## v7.10.6 - 2018-10-03
* Fix formatting of preload examples [Tim Perry]
## v7.10.5 - 2018-09-25
* README: Fix typo [Lucian Buzzo]
## v7.10.4 - 2018-09-24
* Device: When registering, print the uuid [Pablo Carranza Velez]
## v7.10.3 - 2018-09-19
* Include --emulated in the example resin build parameters [Tim Perry]
## v7.10.2 - 2018-09-18
* Dependencies: Update resin-semver version to support Balena OS [Lucian Buzzo]
## v7.10.1 - 2018-09-11
* Stop Travis deploying to npm (now handled by concourse) [Tim Perry]
## v7.10.0 - 2018-09-11
* Update resin-cli-form to 2.x [Pagan Gazzard]
## v7.9.4 - 2018-09-10
* Device api keys are no longer used in the registration process [Theodor Gherzan]
## v7.9.3 - 2018-08-20
* Fix configuration hangs with some images by expanding the threadpool #952 [Tim Perry]
## v7.9.2 - 2018-08-15
* Add warning about re-enabling automatic updates #946 [Pagan Gazzard]
## v7.9.1 - 2018-08-15
* Fix errors in `getRequestStream` not being propogated #942 [Pagan Gazzard]
## v7.9.0 - 2018-08-09
* Support emulated and nocache options for remote builds #903 [Cameron Diver]
## v7.8.6 - 2018-08-09
* Fix bug where the sudo helper failed in os initialize #939 [Tim Perry]
## v7.8.5 - 2018-08-09
* Update .resin-sync.yml docs for local push and include example env vars #934 [Tim Perry]
## v7.8.4 - 2018-08-02
* Update klaw #936 [Tim Perry]
## v7.8.3 - 2018-07-25
* Follow links found during builds #931 [Tim Perry]
## v7.8.2 - 2018-07-25
* Update reconfix to fix volume signature errors in local configure #929 [Tim Perry]
## v7.8.1 - 2018-07-20
* Be explicit about how much initial history log tailing includes #930 [Tim Perry]
## v7.8.0 - 2018-07-20
* Add join/leave commands to promote and move devices between platforms #895 [Akis Kesoglou]
## v7.7.4 - 2018-07-17
* Update TypeScript to 2.8.1 #923 [Tim Perry]
* Add --version options to os configure & config generate #923 [Tim Perry]
* Update OS & config actions to the latest SDK #923 [Tim Perry]
## v7.7.3 - 2018-07-13
* Update the deploy key since npm invalidated the old one #927 [Tim Perry]
## v7.7.2 - 2018-07-13
* Pin ext2fs to 1.0.7 to avoid temporary deployment issues #926 [Tim Perry]
## v7.7.1 - 2018-07-12
* Update CLI to SDK v10 (include new API logs) #925 [Tim Perry]
## v7.7.0 - 2018-07-11
* Add --generate-device-api-key parameter to config generate #921 [Tim Perry]
## v7.6.2 - 2018-06-28
* Make local commands more resilient to unnamed containers #910 [Tim Perry]
## v7.6.1 - 2018-06-26
* Make sure 'resin push' is included in the docs #907 [Tim Perry]
## v7.6.0 - 2018-06-20
* Support pinned release preloading #896 [Cameron Diver]
## v7.5.2 - 2018-06-12
* Document Pyhton native build dependency #893 [Tim Perry]
## v7.5.1 - 2018-06-01
* Add a multicontainer caveat to the env var commands #887 [Tim Perry]
## v7.5.0 - 2018-05-31
* Update resin-compose-parse dependency version to 1.10.2 #883 [Ariel Flesler]
## v7.4.1 - 2018-05-24
* Update SDK in resin device(s) to ensure the dashboard URL is correct #879 [Tim Perry]
## v7.4.0 - 2018-05-10
* Add push command which starts a build on remote resin servers #868 [Cameron Diver]
## v7.3.8 - 2018-05-03
* Catch require errors and provide helpful instructions #874 [Tim Perry]
* Inline the entire resin-cli-errors module #874 [Tim Perry]
## v7.3.7 - 2018-04-30
* Pin node types to v9.0.0 to avoid build errors with transient dependencies #871 [Cameron Diver]
## v7.3.6 - 2018-04-30
* Update resin-image-fs to stop non-config commands failing in node 10 #870 [Tim Perry]
## v7.3.5 - 2018-04-18
* Don't show Docker container status from devices, as it can be wrong #858 [Tim Perry]
## v7.3.4 - 2018-04-18
* Include resin compose schemas in the standalone build #865 [Tim Perry]
## v7.3.3 - 2018-04-17
* Don't report lots of user input errors #861 [Tim Perry]
* Include Sentry breadcrumbs for context in error reports #861 [Tim Perry]
* Update to Sentry 2.x #861 [Tim Perry]
## v7.3.2 - 2018-04-16
* Update Dockerode to fix local push issue in standalone builds #862 [Tim Perry]
## v7.3.1 - 2018-04-13
* Update resin-compose-parse to v1.8.1 to fix a problem parsing ports #849 [Pablo Carranza Velez]
## v7.3.0 - 2018-04-12
* Add 'api-key generate' command #854 [Tim Perry]
## v7.2.4 - 2018-04-10
* Explicitly depend on tar-stream #852 [Tim Perry]
* Correct documented node version requirement to 6+ #852 [Tim Perry]
## v7.2.3 - 2018-04-06
* Add a fast build script to package.json #847 [Cameron Diver]
## v7.2.2 - 2018-04-04
* Throw a clear error when logging in with an invalid token #846 [Tim Perry]
## v7.2.1 - 2018-03-29
* Update docker-qemu-transpose to avoid the broken 0.4.1 release #839 [Tim Perry]
## v7.2.0 - 2018-03-29
* Do not require a login for builds #835 [Tim Perry]
* Allow (experimental!) login with API keys #835 [Tim Perry]
## v7.1.6 - 2018-03-29
* Fix build emulation for multi-stage builds #838 [Tim Perry]
## v7.1.5 - 2018-03-27
* Fix crash when an app is not specified for build command #834 [Akis Kesoglou]
## v7.1.4 - 2018-03-26
* Upgrade resin-sync to pull in the fix for #824 #832 [Tim Perry]
## v7.1.3 - 2018-03-23
* Prefix all pine options with '$' in preload to avoid pine warnings. #831 [Alexis Svinartchouk]
## v7.1.2 - 2018-03-23
* Update resin-preload to 6.2.0 and resin-sdk to 9.0.0-beta16 #830 [Alexis Svinartchouk]
## v7.1.1 - 2018-03-22
* Remove explicit anchor links in CLI docs #827 [Zach Walchuk]
## v7.1.0 - 2018-03-22
* Warn early if deploying a multicontainer project to an incompatible app #818 [Akis Kesoglou]
* Add legacy deploy method back #818 [Akis Kesoglou]
## v7.0.7 - 2018-03-20
* Update resin-preload to 6.1.2 #821 [Alexis Svinartchouk]
## v7.0.6 - 2018-03-20
* Make sure image name is all lowercase #815 [Akis Kesoglou]
* Improve handling of build log output #815 [Akis Kesoglou]
## v7.0.5 - 2018-03-15
* Add bash completions #801 [Ronald McCollam]
## v7.0.4 - 2018-03-15
* Generate consistent working anchors for both our md output & resin docs #813 [Tim Perry]
## v7.0.3 - 2018-03-15
* Fix getting window size when theres no TTY attached #812 [Akis Kesoglou]
## v7.0.2 - 2018-03-13
* Update full CLI docs with recent installation improvements too #807 [Tim Perry]
## v7.0.1 - 2018-03-12
* Recommend using unsafe-prem to avoid permission issues on install #805 [Tim Perry]
* Remove unnecessary resin-cli-auth dependency #805 [Tim Perry]
## v7.0.0 - 2018-03-09
* Add docker-compose-aware builds and deployments #792 [Akis Kesoglou]
* *BREAKING*: Remove support for plugins entirely #792 [Tim Perry]
* Update dashboard login to use the multicontainer SDK #792 [Alexis Svinartchouk]
* Multicontainer preload: Update resin-preload to 6.0.0-beta4 #792 [Alexis Svinartchouk]
* Update the keys action to use the multicontainer SDK #792 [Alexis Svinartchouk]
* Require multicontainer SDK #792 [Alexis Svinartchouk]
## v6.13.5 - 2018-03-07
* Fix prettier configuration to avoid linting errors #802 [Tim Perry]
## v6.13.4 - 2018-02-22
* Fix issue where emulated builds broke Docker `ENV` commands #796 [Gergely Imreh]
## v6.13.3 - 2018-02-20
* Tweak TS & add missing deps that may cause build failures in some envs #793 [Tim Perry]
## v6.13.2 - 2018-02-20
* Ensure login does not wait for the browser process to close #794 [Tim Perry]
## v6.13.1 - 2018-02-07
* Move to the correct coffeescript (no hyphen) dependency #786 [Tim Perry]

View File

@ -13,7 +13,7 @@ Requisites
If you want to install the CLI directly through npm, you'll need the below. If this looks difficult,
we do now have an experimental standalone binary release available, see ['Standalone install'](#standalone-install) below.
- [NodeJS](https://nodejs.org) (>= v4)
- [NodeJS](https://nodejs.org) (>= v6)
- [Git](https://git-scm.com)
- The following executables should be correctly installed in your shell environment:
- `ssh`: Any recent version of the OpenSSH ssh client (required by `resin sync` and `resin ssh`)
@ -45,28 +45,33 @@ or if you have any trouble with this, please try the new standalone install step
This might require elevated privileges in some environments.
```sh
$ npm install --global --production resin-cli
$ npm install resin-cli -g --production --unsafe-perm
```
`--unsafe-perm` is only required on systems where the global install directory is not user-writable.
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
In some environments, this process will need to build native modules. This may require a more complex build
environment, and notably requires Python 2.7. If you hit any problems with this, we recommend you try the
alternative standalone install below instead.
### Standalone install
If you don't have node or a working pre-gyp environment, you can still install the CLI as a standalone
binary. **This is in experimental and may not work perfectly yet in all environments**, but it seems to work
binary. **This is experimental and may not work perfectly yet in all environments**, but it seems to work
well in initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
To install the CLI as a standalone binary:
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
* Extract the contents, putting the `resin-cli` folder somewhere appropriate for your system (e.g. `C:/resin-cli`, `/usr/local/lib/resin-cli`, etc).
* Add the `resin-cli` folder to your `PATH`. (
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
* Add the `resin-cli` folder to your `PATH` ([Windows instructions](https://www.computerhope.com/issues/ch000549.htm), [Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix), [OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
* Running `resin` in a fresh command line should print the resin CLI help.
To update in future, simply download a new release and replace the extracted folder.
Have any problems, or see any unexpected behaviour? Please file an issue!
Have any problems, or see any unexpected behaviour? [Please file an issue!](https://github.com/resin-io/resin-cli/issues/new)
### Login
@ -81,12 +86,9 @@ _(Typically useful, but not strictly required for all commands)_
Take a look at the full command documentation at [https://docs.resin.io/tools/cli/](https://docs.resin.io/tools/cli/#table-of-contents
), or by running `resin help`.
---
### Bash completions
Plugins
-------
The Resin CLI can be extended with plugins to automate laborious tasks and overall provide a better experience when working with Resin.io. Check the [plugin development tutorial](https://github.com/resin-io/resin-plugin-hello) to learn how to build your own!
Optionally you can enable tab completions for the bash shell, enabling the shell to provide additional context and automatically complete arguments to`resin`. To enable bash completions, copy the `resin-completion.bash` file to the default bash completions directory (usually `/etc/bash_completion.d/`) or append it to the end of `~/.bash_completion`.
FAQ
---

View File

@ -36,11 +36,11 @@ function getAnchor(command: Command) {
'#' +
command.signature
.replace(/\s/g, '-')
.replace(/</g, '60-')
.replace(/>/g, '-62-')
.replace(/\[/g, '')
.replace(/</g, '-')
.replace(/>/g, '-')
.replace(/\[/g, '-')
.replace(/\]/g, '-')
.replace(/--/g, '-')
.replace(/-+/g, '-')
.replace(/\.\.\./g, '')
.replace(/\|/g, '')
.toLowerCase()

View File

@ -1,2 +1,7 @@
#!/usr/bin/env node
// We boost the threadpool size as ext2fs can deadlock with some
// operations otherwise, if the pool runs out.
process.env.UV_THREADPOOL_SIZE = '64';
require('../build/app');

View File

@ -1,115 +0,0 @@
# coffeelint: disable=max_line_length
module.exports =
title: 'Resin CLI Documentation'
introduction: '''
This tool allows you to interact with the resin.io api from the comfort of your command line.
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
To get started download the CLI from npm.
$ npm install resin-cli -g
Then authenticate yourself:
$ resin login
Now you have access to all the commands referenced below.
## Proxy support
The CLI does support HTTP(S) proxies.
You can configure the proxy using several methods (in order of their precedence):
* set the `RESINRC_PROXY` environment variable in the URL format (with protocol, host, port, and optionally the basic auth),
* use the [resin config file](https://www.npmjs.com/package/resin-settings-client#documentation) (project-specific or user-level)
and set the `proxy` setting. This can be:
* a string in the URL format,
* or an object following [this format](https://www.npmjs.com/package/global-tunnel-ng#options), which allows more control,
* or set the conventional `https_proxy` / `HTTPS_PROXY` / `http_proxy` / `HTTP_PROXY`
environment variable (in the same standard URL format).
'''
categories: [
{
title: 'Application'
files: [ 'lib/actions/app.coffee' ]
},
{
title: 'Authentication',
files: [ 'lib/actions/auth.coffee' ]
},
{
title: 'Device',
files: [ 'lib/actions/device.coffee' ]
},
{
title: 'Environment Variables',
files: [ 'lib/actions/environment-variables.coffee' ]
},
{
title: 'Help',
files: [ 'lib/actions/help.coffee' ]
},
{
title: 'Information',
files: [ 'lib/actions/info.coffee' ]
},
{
title: 'Keys',
files: [ 'lib/actions/keys.coffee' ]
},
{
title: 'Logs',
files: [ 'lib/actions/logs.coffee' ]
},
{
title: 'Sync',
files: [ 'lib/actions/sync.coffee' ]
},
{
title: 'SSH',
files: [ 'lib/actions/ssh.coffee' ]
},
{
title: 'Notes',
files: [ 'lib/actions/notes.coffee' ]
},
{
title: 'OS',
files: [ 'lib/actions/os.coffee' ]
},
{
title: 'Config',
files: [ 'lib/actions/config.coffee' ]
},
{
title: 'Preload',
files: [ 'lib/actions/preload.coffee' ]
},
{
title: 'Settings',
files: [ 'lib/actions/settings.coffee' ]
},
{
title: 'Wizard',
files: [ 'lib/actions/wizard.coffee' ]
},
{
title: 'Local',
files: [ 'lib/actions/local/index.coffee' ]
},
{
title: 'Deploy',
files: [
'lib/actions/build.coffee'
'lib/actions/deploy.coffee'
]
},
{
title: 'Utilities',
files: [ 'lib/actions/util.coffee' ]
},
]

View File

@ -5,15 +5,47 @@ This tool allows you to interact with the resin.io api from the comfort of your
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
To get started download the CLI from npm.
## Install the CLI
$ npm install resin-cli -g
### Npm install
Then authenticate yourself:
The best supported way to install the CLI is from npm:
$ npm install resin-cli -g --production --unsafe-perm
\`--unsafe-perm\` is only required on systems where the global install directory is not user-writable.
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
### Standalone install
Alternatively, if you don't have a node or pre-gyp environment, you can still install the CLI as a standalone
binary. **This is in experimental and may not work perfectly yet in all environments**, but works well in
initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
To install the CLI as a standalone binary:
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
* Extract the contents, putting the \`resin-cli\` folder somewhere appropriate for your system (e.g. \`C:/resin-cli\`, \`/usr/local/lib/resin-cli\`, etc).
* Add the \`resin-cli\` folder to your \`PATH\`. (
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
* Running \`resin\` in a fresh command line should print the resin CLI help.
To update in future, simply download a new release and replace the extracted folder.
Have any problems, or see any unexpected behaviour? Please file an issue!
## Getting started
Once you have the CLI installed, you'll need to log in, so it can access everything in your resin.io account.
To authenticate yourself, run:
$ resin login
Now you have access to all the commands referenced below.
You now have access to all the commands referenced below.
## Proxy support
@ -31,6 +63,10 @@ environment variable (in the same standard URL format).\
`,
categories: [
{
title: 'Api keys',
files: [ 'build/actions/api-key.js' ],
},
{
title: 'Application',
files: [ 'build/actions/app.js' ]
@ -87,6 +123,10 @@ environment variable (in the same standard URL format).\
title: 'Preload',
files: [ 'build/actions/preload.js' ]
},
{
title: 'Push',
files: [ 'build/actions/push.js' ]
},
{
title: 'Settings',
files: [ 'build/actions/settings.js' ]

View File

@ -4,15 +4,47 @@ This tool allows you to interact with the resin.io api from the comfort of your
Please make sure your system meets the requirements as specified in the [README](https://github.com/resin-io/resin-cli).
To get started download the CLI from npm.
## Install the CLI
$ npm install resin-cli -g
### Npm install
Then authenticate yourself:
The best supported way to install the CLI is from npm:
$ npm install resin-cli -g --production --unsafe-perm
`--unsafe-perm` is only required on systems where the global install directory is not user-writable.
This allows npm install steps to download and save prebuilt native binaries. You may be able to omit it,
especially if you're using a user-managed node install such as [nvm](https://github.com/creationix/nvm).
### Standalone install
Alternatively, if you don't have a node or pre-gyp environment, you can still install the CLI as a standalone
binary. **This is in experimental and may not work perfectly yet in all environments**, but works well in
initial cross-platform testing, so it may be useful, and we'd love your feedback if you hit any issues.
To install the CLI as a standalone binary:
* Download the latest zip for your OS from https://github.com/resin-io/resin-cli/releases.
* Extract the contents, putting the `resin-cli` folder somewhere appropriate for your system (e.g. `C:/resin-cli`, `/usr/local/lib/resin-cli`, etc).
* Add the `resin-cli` folder to your `PATH`. (
[Windows instructions](https://www.computerhope.com/issues/ch000549.htm),
[Linux instructions](https://stackoverflow.com/questions/14637979/how-to-permanently-set-path-on-linux-unix),
[OSX instructions](https://stackoverflow.com/questions/22465332/setting-path-environment-variable-in-osx-permanently))
* Running `resin` in a fresh command line should print the resin CLI help.
To update in future, simply download a new release and replace the extracted folder.
Have any problems, or see any unexpected behaviour? Please file an issue!
## Getting started
Once you have the CLI installed, you'll need to log in, so it can access everything in your resin.io account.
To authenticate yourself, run:
$ resin login
Now you have access to all the commands referenced below.
You now have access to all the commands referenced below.
## Proxy support
@ -30,13 +62,17 @@ environment variable (in the same standard URL format).
# Table of contents
- Api keys
- [api-key generate &#60;name&#62;](#api-key-generate-name-)
- Application
- [app create &#60;name&#62;](#app-create-60-name-62-)
- [app create &#60;name&#62;](#app-create-name-)
- [apps](#apps)
- [app &#60;name&#62;](#app-60-name-62-)
- [app restart &#60;name&#62;](#app-restart-60-name-62-)
- [app rm &#60;name&#62;](#app-rm-60-name-62-)
- [app &#60;name&#62;](#app-name-)
- [app restart &#60;name&#62;](#app-restart-name-)
- [app rm &#60;name&#62;](#app-rm-name-)
- Authentication
@ -48,27 +84,27 @@ environment variable (in the same standard URL format).
- Device
- [devices](#devices)
- [device &#60;uuid&#62;](#device-60-uuid-62-)
- [device &#60;uuid&#62;](#device-uuid-)
- [devices supported](#devices-supported)
- [device register &#60;application&#62;](#device-register-60-application-62-)
- [device rm &#60;uuid&#62;](#device-rm-60-uuid-62-)
- [device identify &#60;uuid&#62;](#device-identify-60-uuid-62-)
- [device reboot &#60;uuid&#62;](#device-reboot-60-uuid-62-)
- [device shutdown &#60;uuid&#62;](#device-shutdown-60-uuid-62-)
- [device public-url enable &#60;uuid&#62;](#device-public-url-enable-60-uuid-62-)
- [device public-url disable &#60;uuid&#62;](#device-public-url-disable-60-uuid-62-)
- [device public-url &#60;uuid&#62;](#device-public-url-60-uuid-62-)
- [device public-url status &#60;uuid&#62;](#device-public-url-status-60-uuid-62-)
- [device rename &#60;uuid&#62; [newName]](#device-rename-60-uuid-62-newname-)
- [device move &#60;uuid&#62;](#device-move-60-uuid-62-)
- [device register &#60;application&#62;](#device-register-application-)
- [device rm &#60;uuid&#62;](#device-rm-uuid-)
- [device identify &#60;uuid&#62;](#device-identify-uuid-)
- [device reboot &#60;uuid&#62;](#device-reboot-uuid-)
- [device shutdown &#60;uuid&#62;](#device-shutdown-uuid-)
- [device public-url enable &#60;uuid&#62;](#device-public-url-enable-uuid-)
- [device public-url disable &#60;uuid&#62;](#device-public-url-disable-uuid-)
- [device public-url &#60;uuid&#62;](#device-public-url-uuid-)
- [device public-url status &#60;uuid&#62;](#device-public-url-status-uuid-)
- [device rename &#60;uuid&#62; [newName]](#device-rename-uuid-newname-)
- [device move &#60;uuid&#62;](#device-move-uuid-)
- [device init](#device-init)
- Environment Variables
- [envs](#envs)
- [env rm &#60;id&#62;](#env-rm-60-id-62-)
- [env add &#60;key&#62; [value]](#env-add-60-key-62-value-)
- [env rename &#60;id&#62; &#60;value&#62;](#env-rename-60-id-62-60-value-62-)
- [env rm &#60;id&#62;](#env-rm-id-)
- [env add &#60;key&#62; [value]](#env-add-key-value-)
- [env rename &#60;id&#62; &#60;value&#62;](#env-rename-id-value-)
- Help
@ -81,13 +117,13 @@ environment variable (in the same standard URL format).
- Keys
- [keys](#keys)
- [key &#60;id&#62;](#key-60-id-62-)
- [key rm &#60;id&#62;](#key-rm-60-id-62-)
- [key add &#60;name&#62; [path]](#key-add-60-name-62-path-)
- [key &#60;id&#62;](#key-id-)
- [key rm &#60;id&#62;](#key-rm-id-)
- [key add &#60;name&#62; [path]](#key-add-name-path-)
- Logs
- [logs &#60;uuid&#62;](#logs-60-uuid-62-)
- [logs &#60;uuid&#62;](#logs-uuid-)
- Sync
@ -99,27 +135,31 @@ environment variable (in the same standard URL format).
- Notes
- [note &#60;|note&#62;](#note-60-note-62-)
- [note &#60;|note&#62;](#note-note-)
- OS
- [os versions &#60;type&#62;](#os-versions-60-type-62-)
- [os download &#60;type&#62;](#os-download-60-type-62-)
- [os build-config &#60;image&#62; &#60;device-type&#62;](#os-build-config-60-image-62-60-device-type-62-)
- [os configure &#60;image&#62; [uuid] [deviceApiKey]](#os-configure-60-image-62-uuid-deviceapikey-)
- [os initialize &#60;image&#62;](#os-initialize-60-image-62-)
- [os versions &#60;type&#62;](#os-versions-type-)
- [os download &#60;type&#62;](#os-download-type-)
- [os build-config &#60;image&#62; &#60;device-type&#62;](#os-build-config-image-device-type-)
- [os configure &#60;image&#62; [uuid] [deviceApiKey]](#os-configure-image-uuid-deviceapikey-)
- [os initialize &#60;image&#62;](#os-initialize-image-)
- Config
- [config read](#config-read)
- [config write &#60;key&#62; &#60;value&#62;](#config-write-60-key-62-60-value-62-)
- [config inject &#60;file&#62;](#config-inject-60-file-62-)
- [config write &#60;key&#62; &#60;value&#62;](#config-write-key-value-)
- [config inject &#60;file&#62;](#config-inject-file-)
- [config reconfigure](#config-reconfigure)
- [config generate](#config-generate)
- Preload
- [preload &#60;image&#62;](#preload-60-image-62-)
- [preload &#60;image&#62;](#preload-image-)
- Push
- [push &#60;applicationOrDevice&#62;](#push-applicationordevice-)
- Settings
@ -131,8 +171,8 @@ environment variable (in the same standard URL format).
- Local
- [local configure &#60;target&#62;](#local-configure-60-target-62-)
- [local flash &#60;image&#62;](#local-flash-60-image-62-)
- [local configure &#60;target&#62;](#local-configure-target-)
- [local flash &#60;image&#62;](#local-flash-image-)
- [local logs [deviceIp]](#local-logs-deviceip-)
- [local scan](#local-scan)
- [local ssh [deviceIp]](#local-ssh-deviceip-)
@ -142,12 +182,26 @@ environment variable (in the same standard URL format).
- Deploy
- [build [source]](#build-source-)
- [deploy &#60;appName&#62; [image]](#deploy-60-appname-62-image-)
- [deploy &#60;appName&#62; [image]](#deploy-appname-image-)
- Utilities
- [util available-drives](#util-available-drives)
# Api keys
## api-key generate &#60;name&#62;
This command generates a new API key for the current user, with the given
name. The key will be logged to the console.
This key can be used to log into the CLI using 'resin login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
Examples:
$ resin api-key generate "Jenkins Key"
# Application
## app create &#60;name&#62;
@ -230,7 +284,7 @@ from the dashboard.
- Credentials: using email/password and 2FA.
- Token: using the authentication token from the preferences page.
- Token: using a session token or API key (experimental) from the preferences page.
Examples:
@ -244,7 +298,7 @@ Examples:
#### --token, -t &#60;token&#62;
auth token
session token or API key (experimental)
#### --web, -w
@ -334,13 +388,10 @@ Examples:
Use this command to register a device to an application.
Note that device api keys are only supported on ResinOS 2.0.3+
Examples:
$ resin device register MyApp
$ resin device register MyApp --uuid <uuid>
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
### Options
@ -348,10 +399,6 @@ Examples:
custom uuid
#### --deviceApiKey, -k &#60;device-api-key&#62;
custom device key - note that this is only supported on ResinOS 2.0.3+
## device rm &#60;uuid&#62;
Use this command to remove a device from resin.io.
@ -521,6 +568,10 @@ This command lists all custom environment variables.
If you want to see all environment variables, including private
ones used by resin, use the verbose option.
At the moment the CLI doesn't fully support multi-container applications,
so the following commands will only show service variables,
without showing which service they belong to.
Example:
$ resin envs --application MyApp
@ -572,6 +623,10 @@ device
Use this command to add an enviroment variable to an application.
At the moment the CLI doesn't fully support multi-container applications,
so the following commands will only set service variables for the first
service in your application.
If value is omitted, the tool will attempt to use the variable's value
as defined in your host machine.
@ -695,10 +750,6 @@ By default, the command prints all log messages and exit.
To continuously stream output, and see new logs in real time, use the `--tail` option.
Note that for now you need to provide the whole UUID for this command to work correctly.
This is due to some technical limitations that we plan to address soon.
Examples:
$ resin logs 23c73a1
@ -919,17 +970,20 @@ the path to the output JSON file
Use this command to configure a previously downloaded operating system image for
the specific device or for an application generally.
Calling this command without --version is not recommended, and may fail in
future releases if the OS version cannot be inferred.
Note that device api keys are only supported on ResinOS 2.0.3+.
This comand still supports the *deprecated* format where the UUID and optionally device key
This command still supports the *deprecated* format where the UUID and optionally device key
are passed directly on the command line, but the recommended way is to pass either an --app or
--device argument. The deprecated format will be remove in a future release.
Examples:
$ resin os configure ../path/rpi.img --device 7cf02a6
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
$ resin os configure ../path/rpi.img --app MyApp
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ resin os configure ../path/rpi.img --app MyApp --version 2.12.7
### Options
@ -949,6 +1003,10 @@ device uuid
custom device key - note that this is only supported on ResinOS 2.0.3+
#### --version &#60;version&#62;
a resinOS version
#### --config &#60;config&#62;
path to the config JSON file, see `resin os build-config`
@ -1067,21 +1125,29 @@ show advanced commands
Use this command to generate a config.json for a device or application.
Calling this command without --version is not recommended, and may fail in
future releases if the OS version cannot be inferred.
This is interactive by default, but you can do this automatically without interactivity
by specifying an option for each question on the command line, if you know the questions
that will be asked for the relevant device type.
Examples:
$ resin config generate --device 7cf02a6
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
$ resin config generate --device 7cf02a6 --output config.json
$ resin config generate --app MyApp
$ resin config generate --app MyApp --output config.json
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
$ resin config generate --device 7cf02a6 --version 2.12.7
$ resin config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
$ resin config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ resin config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ resin config generate --app MyApp --version 2.12.7
$ resin config generate --app MyApp --version 2.12.7 --output config.json
$ resin config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
### Options
#### --version &#60;version&#62;
a resinOS version
#### --application, -a, --app &#60;application&#62;
application name
@ -1094,6 +1160,10 @@ device uuid
custom device key - note that this is only supported on ResinOS 2.0.3+
#### --generate-device-api-key
generate a fresh device key for the device
#### --output, -o &#60;output&#62;
output
@ -1123,12 +1193,12 @@ your shell environment. For more information (including Windows support)
please check the README here: https://github.com/resin-io/resin-cli .
Use this command to preload an application to a local disk image (or
Edison zip archive) with a built commit from Resin.io.
This can be used with cloud builds, or images deployed with resin deploy.
Edison zip archive) with a built release from Resin.io.
Examples:
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
$ resin preload resin.img
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
$ resin preload resin.img
### Options
@ -1138,16 +1208,20 @@ id of the application to preload
#### --commit, -c &#60;hash&#62;
a specific application commit to preload, use "latest" to specify the latest commit
the commit hash for a specific application release to preload, use "latest" to specify the latest release
(ignored if no appId is given)
#### --splash-image, -s &#60;splashImage.png&#62;
path to a png image to replace the splash screen
#### --dont-check-device-type
#### --dont-check-arch
Disables check for matching device types in image and application
Disables check for matching architecture in image and application
#### --pin-device-to-release, -p
Pin the preloaded device to the preloaded release on provision
#### --docker, -P &#60;docker&#62;
@ -1173,6 +1247,45 @@ Docker host TLS certificate file
Docker host TLS key file
# Push
## push &#60;applicationOrDevice&#62;
This command can be used to start a build on the remote
resin.io cloud builders, or a local mode resin device.
When building on the resin cloud the given source directory will be sent to the
resin.io builder, and the build will proceed. This can be used as a drop-in
replacement for git push to deploy.
When building on a local mode device, the given source directory will be built on
device, and the resulting containers will be run on the device. Logs will be
streamed back from the device as part of the same invocation.
Examples:
$ resin push myApp
$ resin push myApp --source <source directory>
$ resin push myApp -s <source directory>
$ resin push 10.0.0.1
$ resin push 10.0.0.1 --source <source directory>
$ resin push 10.0.0.1 -s <source directory>
### Options
#### --source, -s &#60;source&#62;
The source that should be sent to the resin builder to be built (defaults to the current directory)
#### --emulated, -e
Force an emulated build to occur on the remote builder
#### --nocache, -c
Don't use cache when building this project
# Settings
## settings
@ -1330,12 +1443,14 @@ also change any option by editing '.resin-sync.yml' directly.
Here is an example '.resin-sync.yml' :
$ cat $PWD/.resin-sync.yml
destination: '/usr/src/app'
before: 'echo Hello'
after: 'echo Done'
ignore:
- .git
- node_modules/
local_resinos:
app-name: local-app
build-triggers:
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
environment:
- MY_VARIABLE=123
Command line options have precedence over the ones saved in '.resin-sync.yml'.
@ -1433,17 +1548,24 @@ name of container to stop
## build [source]
Use this command to build a container with a provided docker daemon.
Use this command to build an image or a complete multicontainer project
with the provided docker daemon.
You must provide either an application or a device-type/architecture
pair to use the resin Dockerfile pre-processor
(e.g. Dockerfile.template -> Dockerfile).
This command will look into the given source directory (or the current working
directory if one isn't specified) for a compose file. If one is found, this
command will build each service defined in the compose file. If a compose file
isn't found, the command will look for a Dockerfile, and if yet that isn't found,
it will try to generate one.
Examples:
$ resin build
$ resin build ./source/
$ resin build --deviceType raspberrypi3 --arch armhf
$ resin build --deviceType raspberrypi3 --arch armhf --emulated
$ resin build --application MyApp ./source/
$ resin build --docker '/var/run/docker.sock'
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
@ -1462,6 +1584,18 @@ The type of device this build is for
The target resin.io application this build is for
#### --projectName, -n &#60;projectName&#62;
Specify an alternate project name; default is the directory name
#### --emulated, -e
Run an emulated build using Qemu
#### --logs
Display full log output
#### --docker, -P &#60;docker&#62;
Path to a local docker socket
@ -1498,20 +1632,24 @@ Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple time
Don't use docker layer caching when building
#### --emulated, -e
Run an emulated build using Qemu
#### --squash
Squash newly built layers into a single new layer
## deploy &#60;appName&#62; [image]
Use this command to deploy an image to an application, optionally building it first.
Use this command to deploy an image or a complete multicontainer project
to an application, optionally building it first.
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
Unless an image is specified, this command will look into the current directory
(or the one specified by --source) for a compose file. If one is found, this
command will deploy each service defined in the compose file, building it first
if an image for it doesn't exist. If a compose file isn't found, the command
will look for a Dockerfile, and if yet that isn't found, it will try to
generate one.
To deploy to an app on which you're a collaborator, use
`resin deploy <appOwnerUsername>/<appName>`.
@ -1519,23 +1657,37 @@ Note: If building with this command, all options supported by `resin build`
are also supported with this command.
Examples:
$ resin deploy myApp
$ resin deploy myApp --build --source myBuildDir/
$ resin deploy myApp myApp/myImage
### Options
#### --build, -b
Build image then deploy
#### --source, -s &#60;source&#62;
The source directory to use when building the image
Specify an alternate source directory; default is the working directory
#### --build, -b
Force a rebuild before deploy
#### --nologupload
Don't upload build logs to the dashboard with image (if building)
#### --projectName, -n &#60;projectName&#62;
Specify an alternate project name; default is the directory name
#### --emulated, -e
Run an emulated build using Qemu
#### --logs
Display full log output
#### --docker, -P &#60;docker&#62;
Path to a local docker socket
@ -1572,10 +1724,6 @@ Set a build-time variable (eg. "-B 'ARG=value'"). Can be specified multiple time
Don't use docker layer caching when building
#### --emulated, -e
Run an emulated build using Qemu
#### --squash
Squash newly built layers into a single new layer

View File

@ -28,7 +28,7 @@ gulp.task 'coffee', ->
gulp.task 'test', ->
gulp.src(OPTIONS.files.tests, read: false)
.pipe(mocha({
reporter: 'min'
reporter: 'spec'
}))
gulp.task 'build', [

36
lib/actions/api-key.ts Normal file
View File

@ -0,0 +1,36 @@
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
export const generate: CommandDefinition<{
name: string;
}> = {
signature: 'api-key generate <name>',
description: 'Generate a new API key with the given name',
help: stripIndent`
This command generates a new API key for the current user, with the given
name. The key will be logged to the console.
This key can be used to log into the CLI using 'resin login --token <key>',
or to authenticate requests to the API with an 'Authorization: Bearer <key>' header.
Examples:
$ resin api-key generate "Jenkins Key"
`,
async action(params, _options, done) {
const resin = (await import('resin-sdk')).fromSharedOptions();
resin.models.apiKey
.create(params.name)
.then(key => {
console.log(stripIndent`
Registered api key '${params.name}':
${key}
This key will not be shown again, so please save it now.
`);
})
.finally(done);
},
};

View File

@ -52,7 +52,7 @@ exports.create =
# https://github.com/resin-io/resin-cli/issues/30
resin.models.application.has(params.name).then (hasApplication) ->
if hasApplication
throw new Error('You already have an application with that name!')
patterns.exitWithExpectedError('You already have an application with that name!')
.then ->
return options.type or patterns.selectDeviceType()

View File

@ -27,7 +27,7 @@ exports.login =
- Credentials: using email/password and 2FA.
- Token: using the authentication token from the preferences page.
- Token: using a session token or API key (experimental) from the preferences page.
Examples:
@ -40,7 +40,7 @@ exports.login =
options: [
{
signature: 'token'
description: 'auth token'
description: 'session token or API key (experimental)'
parameter: 'token'
alias: 't'
}
@ -73,7 +73,7 @@ exports.login =
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
auth = require('../auth')
form = require('resin-cli-form')
patterns = require('../utils/patterns')
@ -84,10 +84,15 @@ exports.login =
return Promise.try ->
return options.token if _.isString(options.token)
return form.ask
message: 'Token (from the preferences page)'
message: 'Session token or API key (experimental) from the preferences page'
name: 'token'
type: 'input'
.then(resin.auth.loginWithToken)
.tap ->
resin.auth.whoami()
.then (username) ->
if !username
patterns.exitWithExpectedError('Token authentication failed')
else if options.credentials
return patterns.authenticate(options)
else if options.web
@ -97,8 +102,8 @@ exports.login =
return patterns.askLoginType().then (loginType) ->
if loginType is 'register'
capitanoRunAsync = Promise.promisify(require('capitano').run)
return capitanoRunAsync('signup')
{ runCommand } = require('../utils/helpers')
return runCommand('signup')
options[loginType] = true
return login(options)
@ -131,7 +136,7 @@ exports.logout =
$ resin logout
'''
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.auth.logout().nodeify(done)
exports.signup =
@ -152,7 +157,7 @@ exports.signup =
johndoe
'''
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
form = require('resin-cli-form')
validation = require('../utils/validation')
@ -188,7 +193,7 @@ exports.whoami =
permission: 'user'
action: (params, options, done) ->
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
Promise.props

View File

@ -2,42 +2,76 @@
# of this action
Promise = require('bluebird')
dockerUtils = require('../utils/docker')
compose = require('../utils/compose')
getBundleInfo = Promise.method (options) ->
helpers = require('../utils/helpers')
###
Opts must be an object with the following keys:
if options.application?
# An application was provided
return helpers.getArchAndDeviceType(options.application)
.then (app) ->
return [app.arch, app.device_type]
else if options.arch? and options.deviceType?
return [options.arch, options.deviceType]
else
# No information, cannot do resolution
return undefined
app: the app this build is for (optional)
arch: the architecture to build for
deviceType: the device type to build for
buildEmulated
buildOpts: arguments to forward to docker build command
###
buildProject = (docker, logger, composeOpts, opts) ->
compose.loadProject(
logger
composeOpts.projectPath
composeOpts.projectName
)
.then (project) ->
appType = opts.app?.application_type?[0]
if appType? and project.descriptors.length > 1 and not appType.supports_multicontainer
logger.logWarn(
'Target application does not support multiple containers.\n' +
'Continuing with build, but you will not be able to deploy.'
)
compose.buildProject(
docker
logger
project.path
project.name
project.composition
opts.arch
opts.deviceType
opts.buildEmulated
opts.buildOpts
composeOpts.inlineLogs
)
.then ->
logger.logSuccess('Build succeeded!')
.tapCatch (e) ->
logger.logError('Build failed')
module.exports =
signature: 'build [source]'
description: 'Build a container locally'
permission: 'user'
description: 'Build a single image or a multicontainer project locally'
primary: true
help: '''
Use this command to build a container with a provided docker daemon.
Use this command to build an image or a complete multicontainer project
with the provided docker daemon.
You must provide either an application or a device-type/architecture
pair to use the resin Dockerfile pre-processor
(e.g. Dockerfile.template -> Dockerfile).
This command will look into the given source directory (or the current working
directory if one isn't specified) for a compose file. If one is found, this
command will build each service defined in the compose file. If a compose file
isn't found, the command will look for a Dockerfile, and if yet that isn't found,
it will try to generate one.
Examples:
$ resin build
$ resin build ./source/
$ resin build --deviceType raspberrypi3 --arch armhf
$ resin build --deviceType raspberrypi3 --arch armhf --emulated
$ resin build --application MyApp ./source/
$ resin build --docker '/var/run/docker.sock'
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
'''
options: dockerUtils.appendOptions [
options: dockerUtils.appendOptions compose.appendOptions [
{
signature: 'arch'
parameter: 'arch'
@ -58,7 +92,54 @@ module.exports =
},
]
action: (params, options, done) ->
Logger = require('../utils/logger')
dockerUtils.runBuild(params, options, getBundleInfo, new Logger())
.asCallback(done)
# compositions with many services trigger misleading warnings
require('events').defaultMaxListeners = 1000
{ exitWithExpectedError } = require('../utils/patterns')
helpers = require('../utils/helpers')
Logger = require('../utils/logger')
logger = new Logger()
logger.logDebug('Parsing input...')
Promise.try ->
# `build` accepts `[source]` as a parameter, but compose expects it
# as an option. swap them here
options.source ?= params.source
delete params.source
{ application, arch, deviceType } = options
if (not (arch? and deviceType?) and not application?) or (application? and (arch? or deviceType?))
exitWithExpectedError('You must specify either an application or an arch/deviceType pair to build for')
if arch? and deviceType?
[ undefined, arch, deviceType ]
else
Promise.join(
helpers.getApplication(application)
helpers.getArchAndDeviceType(application)
(app, { arch, device_type }) ->
app.arch = arch
app.device_type = device_type
return app
)
.then (app) ->
[ app, app.arch, app.device_type ]
.then ([ app, arch, deviceType ]) ->
Promise.join(
dockerUtils.getDocker(options)
dockerUtils.generateBuildOpts(options)
compose.generateOpts(options)
(docker, buildOpts, composeOpts) ->
buildProject(docker, logger, composeOpts, {
app
arch
deviceType
buildEmulated: !!options.emulated
buildOpts
})
)
.asCallback(done)

View File

@ -44,6 +44,11 @@ exports.optionalDeviceApiKey =
parameter: 'device-api-key'
alias: 'k'
exports.optionalOsVersion =
signature: 'version'
description: 'a resinOS version'
parameter: 'version'
exports.booleanDevice =
signature: 'device'
description: 'device'

View File

@ -198,7 +198,7 @@ exports.reconfigure =
Promise = require('bluebird')
config = require('resin-config-json')
visuals = require('resin-cli-visuals')
capitanoRunAsync = Promise.promisify(require('capitano').run)
{ runCommand } = require('../utils/helpers')
umountAsync = Promise.promisify(require('umount').umount)
Promise.try ->
@ -212,7 +212,7 @@ exports.reconfigure =
configureCommand = "os configure #{drive} --device #{uuid}"
if options.advanced
configureCommand += ' --advanced'
return capitanoRunAsync(configureCommand)
return runCommand(configureCommand)
.then ->
console.info('Done')
.nodeify(done)
@ -223,23 +223,34 @@ exports.generate =
help: '''
Use this command to generate a config.json for a device or application.
Calling this command without --version is not recommended, and may fail in
future releases if the OS version cannot be inferred.
This is interactive by default, but you can do this automatically without interactivity
by specifying an option for each question on the command line, if you know the questions
that will be asked for the relevant device type.
Examples:
$ resin config generate --device 7cf02a6
$ resin config generate --device 7cf02a6 --device-api-key <existingDeviceKey>
$ resin config generate --device 7cf02a6 --output config.json
$ resin config generate --app MyApp
$ resin config generate --app MyApp --output config.json
$ resin config generate --app MyApp --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
$ resin config generate --device 7cf02a6 --version 2.12.7
$ resin config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key
$ resin config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ resin config generate --device 7cf02a6 --version 2.12.7 --output config.json
$ resin config generate --app MyApp --version 2.12.7
$ resin config generate --app MyApp --version 2.12.7 --output config.json
$ resin config generate --app MyApp --version 2.12.7 \
--network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1
'''
options: [
commandOptions.optionalOsVersion
commandOptions.optionalApplication
commandOptions.optionalDevice
commandOptions.optionalDeviceApiKey
{
signature: 'generate-device-api-key'
description: 'generate a fresh device key for the device'
boolean: true
}
{
signature: 'output'
description: 'output'
@ -273,14 +284,16 @@ exports.generate =
normalizeUuidProp(options, 'device')
Promise = require('bluebird')
writeFileAsync = Promise.promisify(require('fs').writeFile)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
form = require('resin-cli-form')
deviceConfig = require('resin-device-config')
prettyjson = require('prettyjson')
{ generateDeviceConfig, generateApplicationConfig } = require('../utils/config')
{ exitWithExpectedError } = require('../utils/patterns')
if not options.device? and not options.application?
throw new Error '''
exitWithExpectedError '''
You have to pass either a device or an application.
See the help page for examples:
@ -300,8 +313,10 @@ exports.generate =
# required option, that value is used (and the corresponding question is not asked)
form.run(formOptions, override: options)
.then (answers) ->
answers.version = options.version
if resource.uuid?
generateDeviceConfig(resource, options.deviceApiKey, answers)
generateDeviceConfig(resource, options.deviceApiKey || options['generate-device-api-key'], answers)
else
generateApplicationConfig(resource, answers)
.then (config) ->

View File

@ -1,121 +1,139 @@
# Imported here because it's needed for the setup
# of this action
Promise = require('bluebird')
dockerUtils = require('../utils/docker')
compose = require('../utils/compose')
getBuilderPushEndpoint = (baseUrl, owner, app) ->
querystring = require('querystring')
args = querystring.stringify({ owner, app })
"https://builder.#{baseUrl}/v1/push?#{args}"
###
Opts must be an object with the following keys:
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
querystring = require('querystring')
args = querystring.stringify({ owner, app, buildId })
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
app: the application instance to deploy to
image: the image to deploy; optional
shouldPerformBuild
shouldUploadLogs
buildEmulated
buildOpts: arguments to forward to docker build command
###
deployProject = (docker, logger, composeOpts, opts) ->
_ = require('lodash')
doodles = require('resin-doodles')
sdk = require('resin-sdk').fromSharedOptions()
formatImageName = (image) ->
image.split('/').pop()
compose.loadProject(
logger
composeOpts.projectPath
composeOpts.projectName
opts.image
)
.then (project) ->
if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer
throw new Error('Target application does not support multiple containers. Aborting!')
parseInput = Promise.method (params, options) ->
if not params.appName?
throw new Error('Need an application to deploy to!')
appName = params.appName
image = undefined
if params.image?
if options.build or options.source?
throw new Error('Build and source parameters are not applicable when specifying an image')
options.build = false
image = params.image
else if options.build
source = options.source || '.'
else
throw new Error('Need either an image or a build flag!')
# find which services use images that already exist locally
Promise.map project.descriptors, (d) ->
# unconditionally build (or pull) if explicitly requested
return d if opts.shouldPerformBuild
docker.getImage(d.image.tag ? d.image).inspect()
.return(d.serviceName)
.catchReturn()
.filter (d) -> !!d
.then (servicesToSkip) ->
# multibuild takes in a composition and always attempts to
# build or pull all services. we workaround that here by
# passing a modified composition.
compositionToBuild = _.cloneDeep(project.composition)
compositionToBuild.services = _.omit(compositionToBuild.services, servicesToSkip)
if _.size(compositionToBuild.services) is 0
logger.logInfo('Everything is up to date (use --build to force a rebuild)')
return {}
compose.buildProject(
docker
logger
project.path
project.name
compositionToBuild
opts.app.arch
opts.app.device_type
opts.buildEmulated
opts.buildOpts
composeOpts.inlineLogs
)
.then (builtImages) ->
_.keyBy(builtImages, 'serviceName')
.then (builtImages) ->
project.descriptors.map (d) ->
builtImages[d.serviceName] ? {
serviceName: d.serviceName,
name: d.image.tag ? d.image
logs: 'Build skipped; image for service already exists.'
props: {}
}
.then (images) ->
if opts.app.application_type?[0]?.is_legacy
chalk = require('chalk')
legacyDeploy = require('../utils/deploy-legacy')
return [appName, options.build, source, image]
msg = chalk.yellow('Target application requires legacy deploy method.')
logger.logWarn(msg)
showPushProgress = (message) ->
visuals = require('resin-cli-visuals')
progressBar = new visuals.Progress(message)
progressBar.update({ percentage: 0 })
return progressBar
getBundleInfo = (options) ->
helpers = require('../utils/helpers')
helpers.getArchAndDeviceType(options.appName)
.then (app) ->
[app.arch, app.device_type]
performUpload = (imageStream, token, username, url, appName, logger) ->
request = require('request')
progressStream = require('progress-stream')
zlib = require('zlib')
# Need to strip off the newline
progressMessage = logger.formatMessage('info', 'Deploying').slice(0, -1)
progressBar = showPushProgress(progressMessage)
streamWithProgress = imageStream.pipe progressStream
time: 500,
length: imageStream.length
, ({ percentage, eta }) ->
progressBar.update
percentage: Math.min(percentage, 100)
eta: eta
uploadRequest = request.post
url: getBuilderPushEndpoint(url, username, appName)
headers:
'Content-Encoding': 'gzip'
auth:
bearer: token
body: streamWithProgress.pipe(zlib.createGzip({
level: 6
}))
uploadToPromise(uploadRequest, logger)
uploadLogs = (logs, token, url, buildId, username, appName) ->
request = require('request')
request.post
json: true
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
auth:
bearer: token
body: Buffer.from(logs)
uploadToPromise = (uploadRequest, logger) ->
new Promise (resolve, reject) ->
handleMessage = (data) ->
data = data.toString()
logger.logDebug("Received data: #{data}")
try
obj = JSON.parse(data)
catch e
logger.logError('Error parsing reply from remote side')
reject(e)
return
if obj.type?
switch obj.type
when 'error' then reject(new Error("Remote error: #{obj.error}"))
when 'success' then resolve(obj)
when 'status' then logger.logInfo("Remote: #{obj.message}")
else reject(new Error("Received unexpected reply from remote: #{data}"))
else
reject(new Error("Received unexpected reply from remote: #{data}"))
uploadRequest
.on('error', reject)
.on('data', handleMessage)
return Promise.join(
docker
logger
sdk.auth.getToken()
sdk.auth.whoami()
sdk.settings.get('resinUrl')
{
appName: opts.app.app_name
imageName: images[0].name
buildLogs: images[0].logs
shouldUploadLogs: opts.shouldUploadLogs
}
legacyDeploy
)
.then (releaseId) ->
sdk.models.release.get(releaseId, $select: [ 'commit' ])
Promise.join(
sdk.auth.getUserId()
sdk.auth.getToken()
sdk.settings.get('apiUrl')
(userId, auth, apiEndpoint) ->
compose.deployProject(
docker
logger
project.composition
images
opts.app.id
userId
"Bearer #{auth}"
apiEndpoint
!opts.shouldUploadLogs
)
)
.then (release) ->
logger.logSuccess('Deploy succeeded!')
logger.logSuccess("Release: #{release.commit}")
console.log()
console.log(doodles.getDoodle()) # Show charlie
console.log()
.tapCatch (e) ->
logger.logError('Deploy failed')
module.exports =
signature: 'deploy <appName> [image]'
description: 'Deploy an image to a resin.io application'
description: 'Deploy a single image or a multicontainer project to a resin.io application'
help: '''
Use this command to deploy an image to an application, optionally building it first.
Use this command to deploy an image or a complete multicontainer project
to an application, optionally building it first.
Usage: `deploy <appName> ([image] | --build [--source build-dir])`
Unless an image is specified, this command will look into the current directory
(or the one specified by --source) for a compose file. If one is found, this
command will deploy each service defined in the compose file, building it first
if an image for it doesn't exist. If a compose file isn't found, the command
will look for a Dockerfile, and if yet that isn't found, it will try to
generate one.
To deploy to an app on which you're a collaborator, use
`resin deploy <appOwnerUsername>/<appName>`.
@ -123,23 +141,26 @@ module.exports =
are also supported with this command.
Examples:
$ resin deploy myApp
$ resin deploy myApp --build --source myBuildDir/
$ resin deploy myApp myApp/myImage
'''
permission: 'user'
options: dockerUtils.appendOptions [
{
signature: 'build'
boolean: true
description: 'Build image then deploy'
alias: 'b'
},
primary: true
options: dockerUtils.appendOptions compose.appendOptions [
{
signature: 'source'
parameter: 'source'
description: 'The source directory to use when building the image'
description: 'Specify an alternate source directory; default is the working directory'
alias: 's'
},
{
signature: 'build'
boolean: true
description: 'Force a rebuild before deploy'
alias: 'b'
},
{
signature: 'nologupload'
description: "Don't upload build logs to the dashboard with image (if building)"
@ -147,83 +168,53 @@ module.exports =
}
]
action: (params, options, done) ->
_ = require('lodash')
tmp = require('tmp')
tmpNameAsync = Promise.promisify(tmp.tmpName)
resin = require('resin-sdk-preconfigured')
# compositions with many services trigger misleading warnings
require('events').defaultMaxListeners = 1000
helpers = require('../utils/helpers')
Logger = require('../utils/logger')
logger = new Logger()
# Ensure the tmp files gets deleted
tmp.setGracefulCleanup()
logger.logDebug('Parsing input...')
logs = ''
Promise.try ->
{ appName, image } = params
upload = (token, username, url) ->
dockerUtils.getDocker(options)
.then (docker) ->
# Check input parameters
parseInput(params, options)
.then ([appName, build, source, imageName]) ->
tmpNameAsync()
.then (bufferFile) ->
# look into "resin build" options if appName isn't given
appName = options.application if not appName?
delete options.application
# Setup the build args for how the build routine expects them
options = _.assign({}, options, { appName })
params = _.assign({}, params, { source })
if not appName?
throw new Error('Please specify the name of the application to deploy')
Promise.try ->
if build
dockerUtils.runBuild(params, options, getBundleInfo, logger)
else
{ image: imageName, log: '' }
.then ({ image: imageName, log: buildLogs }) ->
logger.logInfo('Initializing deploy...')
if image? and options.build
throw new Error('Build option is not applicable when specifying an image')
logs = buildLogs
Promise.all [
dockerUtils.bufferImage(docker, imageName, bufferFile)
token
username
url
params.appName
logger
]
.spread(performUpload)
.finally ->
# If the file was never written to (for instance because an error
# has occured before any data was written) this call will throw an
# ugly error, just suppress it
Promise.try ->
require('mz/fs').unlink(bufferFile)
.catch(_.noop)
.tap ({ image: imageName, buildId }) ->
logger.logSuccess("Successfully deployed image: #{formatImageName(imageName)}")
return buildId
.then ({ image: imageName, buildId }) ->
if logs is '' or options.nologupload?
return ''
Promise.join(
helpers.getApplication(appName)
helpers.getArchAndDeviceType(appName)
(app, { arch, device_type }) ->
app.arch = arch
app.device_type = device_type
return app
)
.then (app) ->
[ app, image, !!options.build, !options.nologupload ]
logger.logInfo('Uploading logs to dashboard...')
Promise.join(
logs
token
url
buildId
username
params.appName
uploadLogs
)
.return('Successfully uploaded logs')
.then (msg) ->
logger.logSuccess(msg) if msg isnt ''
.asCallback(done)
Promise.join(
resin.auth.getToken()
resin.auth.whoami()
resin.settings.get('resinUrl')
upload
)
.then ([ app, image, shouldPerformBuild, shouldUploadLogs ]) ->
Promise.join(
dockerUtils.getDocker(options)
dockerUtils.generateBuildOpts(options)
compose.generateOpts(options)
(docker, buildOpts, composeOpts) ->
deployProject(docker, logger, composeOpts, {
app
image
shouldPerformBuild
shouldUploadLogs
buildEmulated: !!options.emulated
buildOpts
})
)
.asCallback(done)

View File

@ -18,6 +18,10 @@ commandOptions = require('./command-options')
_ = require('lodash')
{ normalizeUuidProp } = require('../utils/normalization')
expandForAppName = {
$expand: belongs_to__application: $select: 'app_name'
}
exports.list =
signature: 'devices'
description: 'list all devices'
@ -38,23 +42,25 @@ exports.list =
primary: true
action: (params, options, done) ->
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
Promise.try ->
if options.application?
return resin.models.device.getAllByApplication(options.application)
return resin.models.device.getAll()
return resin.models.device.getAllByApplication(options.application, expandForAppName)
return resin.models.device.getAll(expandForAppName)
.tap (devices) ->
devices = _.map devices, (device) ->
device.dashboard_url = resin.models.device.getDashboardUrl(device.uuid)
device.application_name = device.belongs_to__application[0].app_name
device.uuid = device.uuid.slice(0, 7)
return device
console.log visuals.table.horizontal devices, [
'id'
'uuid'
'name'
'device_name'
'device_type'
'application_name'
'status'
@ -79,16 +85,19 @@ exports.info =
primary: true
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
resin.models.device.get(params.uuid).then (device) ->
resin.models.device.get(params.uuid, expandForAppName)
.then (device) ->
resin.models.device.getStatus(device).then (status) ->
device.status = status
device.dashboard_url = resin.models.device.getDashboardUrl(device.uuid)
device.application_name = device.belongs_to__application[0].app_name
device.commit = device.is_on__commit
console.log visuals.table.vertical device, [
"$#{device.name}$"
"$#{device.device_name}$"
'id'
'device_type'
'status'
@ -117,7 +126,7 @@ exports.supported =
$ resin devices supported
'''
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
resin.models.config.getDeviceTypes().then (deviceTypes) ->
@ -133,13 +142,10 @@ exports.register =
help: '''
Use this command to register a device to an application.
Note that device api keys are only supported on ResinOS 2.0.3+
Examples:
$ resin device register MyApp
$ resin device register MyApp --uuid <uuid>
$ resin device register MyApp --uuid <uuid> --device-api-key <existingDeviceKey>
'''
permission: 'user'
options: [
@ -149,23 +155,17 @@ exports.register =
parameter: 'uuid'
alias: 'u'
}
commandOptions.optionalDeviceApiKey
]
action: (params, options, done) ->
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
Promise.join(
resin.models.application.get(params.application)
options.uuid ? resin.models.device.generateUniqueKey()
options.deviceApiKey ? resin.models.device.generateUniqueKey()
(application, uuid, deviceApiKey) ->
(application, uuid) ->
console.info("Registering to #{application.app_name}: #{uuid}")
if not options.deviceApiKey?
console.info("Using generated device api key: #{deviceApiKey}")
else
console.info('Using provided device api key')
return resin.models.device.register(application.id, uuid, deviceApiKey)
return resin.models.device.register(application.id, uuid)
)
.get('uuid')
.nodeify(done)
@ -188,7 +188,7 @@ exports.remove =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
patterns.confirm(options.yes, 'Are you sure you want to delete the device?').then ->
@ -210,7 +210,7 @@ exports.identify =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.identify(params.uuid).nodeify(done)
exports.reboot =
@ -227,7 +227,7 @@ exports.reboot =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.reboot(params.uuid, options).nodeify(done)
exports.shutdown =
@ -244,7 +244,7 @@ exports.shutdown =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.shutdown(params.uuid, options).nodeify(done)
exports.enableDeviceUrl =
@ -260,7 +260,7 @@ exports.enableDeviceUrl =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.enableDeviceUrl(params.uuid).nodeify(done)
exports.disableDeviceUrl =
@ -276,7 +276,7 @@ exports.disableDeviceUrl =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.disableDeviceUrl(params.uuid).nodeify(done)
exports.getDeviceUrl =
@ -292,7 +292,7 @@ exports.getDeviceUrl =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.getDeviceUrl(params.uuid).then (url) ->
console.log(url)
.nodeify(done)
@ -310,7 +310,7 @@ exports.hasDeviceUrl =
permission: 'user'
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.device.hasDeviceUrl(params.uuid).then (hasDeviceUrl) ->
console.log(hasDeviceUrl)
.nodeify(done)
@ -332,7 +332,7 @@ exports.rename =
action: (params, options, done) ->
normalizeUuidProp(params)
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
form = require('resin-cli-form')
Promise.try ->
@ -362,14 +362,14 @@ exports.move =
options: [ commandOptions.optionalApplication ]
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
resin.models.device.get(params.uuid).then (device) ->
resin.models.device.get(params.uuid, expandForAppName).then (device) ->
return options.application or patterns.selectApplication (application) ->
return _.every [
application.device_type is device.device_type
device.application_name isnt application.app_name
device.belongs_to__application[0].app_name isnt application.app_name
]
.tap (application) ->
return resin.models.device.move(params.uuid, application)
@ -406,14 +406,14 @@ exports.init =
permission: 'user'
action: (params, options, done) ->
Promise = require('bluebird')
capitanoRunAsync = Promise.promisify(require('capitano').run)
rimraf = Promise.promisify(require('rimraf'))
tmp = require('tmp')
tmpNameAsync = Promise.promisify(tmp.tmpName)
tmp.setGracefulCleanup()
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
{ runCommand } = require('../utils/helpers')
Promise.try ->
return options.application if options.application?
@ -424,12 +424,12 @@ exports.init =
download = ->
tmpNameAsync().then (tempPath) ->
osVersion = options['os-version'] or 'default'
capitanoRunAsync("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
runCommand("os download #{application.device_type} --output '#{tempPath}' --version #{osVersion}")
.disposer (tempPath) ->
return rimraf(tempPath)
Promise.using download(), (tempPath) ->
capitanoRunAsync("device register #{application.app_name}")
runCommand("device register #{application.app_name}")
.then(resin.models.device.get)
.tap (device) ->
configureCommand = "os configure '#{tempPath}' --device #{device.uuid}"
@ -437,14 +437,14 @@ exports.init =
configureCommand += " --config '#{options.config}'"
else if options.advanced
configureCommand += ' --advanced'
capitanoRunAsync(configureCommand)
runCommand(configureCommand)
.then ->
osInitCommand = "os initialize '#{tempPath}' --type #{application.device_type}"
if options.yes
osInitCommand += ' --yes'
if options.drive
osInitCommand += " --drive #{options.drive}"
capitanoRunAsync(osInitCommand)
runCommand(osInitCommand)
# Make sure the device resource is removed if there is an
# error when configuring or initializing a device image
.catch (error) ->

View File

@ -28,6 +28,10 @@ exports.list =
If you want to see all environment variables, including private
ones used by resin, use the verbose option.
At the moment the CLI doesn't fully support multi-container applications,
so the following commands will only show service variables,
without showing which service they belong to.
Example:
$ resin envs --application MyApp
@ -53,17 +57,19 @@ exports.list =
resin = require('resin-sdk-preconfigured')
visuals = require('resin-cli-visuals')
{ exitWithExpectedError } = require('../utils/patterns')
Promise.try ->
if options.application?
return resin.models.environmentVariables.getAllByApplication(options.application)
else if options.device?
return resin.models.environmentVariables.device.getAll(options.device)
else
throw new Error('You must specify an application or device')
exitWithExpectedError('You must specify an application or device')
.tap (environmentVariables) ->
if _.isEmpty(environmentVariables)
throw new Error('No environment variables found')
exitWithExpectedError('No environment variables found')
if not options.verbose
isSystemVariable = resin.models.environmentVariables.isSystemVariable
environmentVariables = _.reject(environmentVariables, isSystemVariable)
@ -116,6 +122,10 @@ exports.add =
help: '''
Use this command to add an enviroment variable to an application.
At the moment the CLI doesn't fully support multi-container applications,
so the following commands will only set service variables for the first
service in your application.
If value is omitted, the tool will attempt to use the variable's value
as defined in your host machine.
@ -141,6 +151,8 @@ exports.add =
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
{ exitWithExpectedError } = require('../utils/patterns')
Promise.try ->
if not params.value?
params.value = process.env[params.key]
@ -155,7 +167,7 @@ exports.add =
else if options.device?
resin.models.environmentVariables.device.create(options.device, params.key, params.value)
else
throw new Error('You must specify an application or device')
exitWithExpectedError('You must specify an application or device')
.nodeify(done)
exports.rename =

View File

@ -18,6 +18,7 @@ _ = require('lodash')
capitano = require('capitano')
columnify = require('columnify')
messages = require('../utils/messages')
{ exitWithExpectedError } = require('../utils/patterns')
parse = (object) ->
return _.fromPairs _.map object, (item) ->
@ -55,8 +56,6 @@ general = (params, options, done) ->
return command.hidden or command.isWildcard()
groupedCommands = _.groupBy commands, (command) ->
if command.plugin
return 'plugins'
if command.primary
return 'primary'
return 'secondary'
@ -64,10 +63,6 @@ general = (params, options, done) ->
print(parse(groupedCommands.primary))
if options.verbose
if not _.isEmpty(groupedCommands.plugins)
console.log('\nInstalled plugins:\n')
print(parse(groupedCommands.plugins))
console.log('\nAdditional commands:\n')
print(parse(groupedCommands.secondary))
else
@ -84,7 +79,7 @@ command = (params, options, done) ->
return done(error) if error?
if not command? or command.isWildcard()
return done(new Error("Command not found: #{params.command}"))
exitWithExpectedError("Command not found: #{params.command}")
console.log("Usage: #{command.signature}")

View File

@ -16,9 +16,10 @@ limitations under the License.
module.exports =
wizard: require('./wizard')
apiKey: require('./api-key')
app: require('./app')
info: require('./info')
auth: require('./auth')
info: require('./info')
device: require('./device')
env: require('./environment-variables')
keys: require('./keys')
@ -36,3 +37,6 @@ module.exports =
deploy: require('./deploy')
util: require('./util')
preload: require('./preload')
push: require('./push')
join: require('./join')
leave: require('./leave')

View File

@ -35,3 +35,53 @@ exports.osInit =
init.initialize(params.image, params.type, config)
.then(helpers.osProgressHandler)
.nodeify(done)
exports.scanDevices =
signature: 'internal scandevices'
description: 'scan for local resin-enabled devices and show a picker to choose one'
help: '''
Don't use this command directly!
'''
hidden: true
root: true
action: (params, options, done) ->
Promise = require('bluebird')
{ forms } = require('resin-sync')
return Promise.try ->
forms.selectLocalResinOsDevice()
.then (hostnameOrIp) ->
console.error("==> Selected device: #{hostnameOrIp}")
.nodeify(done)
exports.sudo =
signature: 'internal sudo <command>'
description: 'execute arbitrary commands in a privileged subprocess'
help: '''
Don't use this command directly!
<command> must be passed as a single argument. That means, you need to make sure
you enclose <command> in quotes (eg. resin internal sudo 'ls -alF') if for
whatever reason you invoke the command directly or, typically, pass <command>
as a single argument to spawn (eg. `spawn('resin', [ 'internal', 'sudo', 'ls -alF' ])`).
Furthermore, this command will naively split <command> on whitespace and directly
forward the parts as arguments to `sudo`, so be careful.
'''
hidden: true
action: (params, options, done) ->
os = require('os')
Promise = require('bluebird')
return Promise.try ->
if os.platform() is 'win32'
windosu = require('windosu')
windosu.exec(params.command, {})
else
{ spawn } = require('child_process')
{ wait } = require('rindle')
cmd = params.command.split(' ')
ps = spawn('sudo', cmd, stdio: 'inherit', env: process.env)
wait(ps)
.nodeify(done)

62
lib/actions/join.ts Normal file
View File

@ -0,0 +1,62 @@
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
interface Options {
application?: string;
}
export const join: CommandDefinition<Args, Options> = {
signature: 'join [deviceIp]',
description:
'Promote a local device running unmanaged resinOS to join a resin.io application',
help: stripIndent`
Examples:
$ resin join
$ resin join resin.local
$ resin join resin.local --application MyApp
$ resin join 192.168.1.25
$ resin join 192.168.1.25 --application MyApp
`,
options: [
{
signature: 'application',
parameter: 'application',
alias: 'a',
description: 'The name of the application the device should join',
},
],
primary: true,
async action(params, options, done) {
const resin = await import('resin-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = resin.fromSharedOptions();
const logger = new Logger();
return Bluebird.try(() => {
return promote.join(logger, sdk, params.deviceIp, options.application);
}).nodeify(done);
},
};

View File

@ -28,7 +28,7 @@ exports.list =
'''
permission: 'user'
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
resin.models.key.getAll().then (keys) ->
@ -50,7 +50,7 @@ exports.info =
'''
permission: 'user'
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
visuals = require('resin-cli-visuals')
resin.models.key.get(params.id).then (key) ->
@ -82,7 +82,7 @@ exports.remove =
options: [ commandOptions.yes ]
permission: 'user'
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
patterns.confirm(options.yes, 'Are you sure you want to delete the key?').then ->
@ -109,7 +109,7 @@ exports.add =
Promise = require('bluebird')
readFileAsync = Promise.promisify(require('fs').readFile)
capitano = require('capitano')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
Promise.try ->
return readFileAsync(params.path, encoding: 'utf8') if params.path?

49
lib/actions/leave.ts Normal file
View File

@ -0,0 +1,49 @@
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Bluebird from 'bluebird';
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
interface Args {
deviceIp?: string;
}
export const leave: CommandDefinition<Args, {}> = {
signature: 'leave [deviceIp]',
description: 'Detach a local device from its resin.io application',
help: stripIndent`
Examples:
$ resin leave
$ resin leave resin.local
$ resin leave 192.168.1.25
`,
options: [],
permission: 'user',
primary: true,
async action(params, _options, done) {
const resin = await import('resin-sdk');
const Logger = await import('../utils/logger');
const promote = await import('../utils/promote');
const sdk = resin.fromSharedOptions();
const logger = new Logger();
return Bluebird.try(() => {
return promote.leave(logger, sdk, params.deviceIp);
}).nodeify(done);
},
};

View File

@ -1,9 +1,11 @@
Promise = require('bluebird')
_ = require('lodash')
Docker = require('docker-toolbelt')
form = require('resin-cli-form')
chalk = require('chalk')
dockerUtils = require('../../utils/docker')
{ exitWithExpectedError } = require('../../utils/patterns')
exports.dockerPort = dockerPort = 2375
exports.dockerTimeout = dockerTimeout = 2000
@ -13,7 +15,7 @@ exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container
return true
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
docker = new Docker(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
# List all containers, including those not running
docker.listContainersAsync(all: true)
@ -22,23 +24,22 @@ exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor =
filterOutSupervisorContainer(container)
.then (containers) ->
if _.isEmpty(containers)
throw new Error("No containers found in #{deviceIp}")
exitWithExpectedError("No containers found in #{deviceIp}")
return form.ask
message: 'Select a container'
type: 'list'
choices: _.map containers, (container) ->
containerName = container.Names[0] or 'Untitled'
containerName = container.Names?[0] or 'Untitled'
shortContainerId = ('' + container.Id).substr(0, 11)
containerStatus = container.Status
return {
name: "#{containerName} (#{shortContainerId}) - #{containerStatus}"
name: "#{containerName} (#{shortContainerId})"
value: container.Id
}
exports.pipeContainerStream = Promise.method ({ deviceIp, name, outStream, follow = false }) ->
docker = new Docker(host: deviceIp, port: dockerPort)
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort)
container = docker.getContainer(name)
container.inspectAsync()

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
###
BOOT_PARTITION = { primary: 1 }
BOOT_PARTITION = 1
CONNECTIONS_FOLDER = '/system-connections'
getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
@ -51,6 +51,8 @@ getConfigurationSchema = (connnectionFileName = 'resin-wifi') ->
type: 'ini'
location:
path: CONNECTIONS_FOLDER.slice(1)
# Reconfix still uses the older resin-image-fs, so still needs an
# object-based partition definition.
partition: BOOT_PARTITION
config_json:
type: 'json'

View File

@ -43,12 +43,14 @@ resinPushHelp = '''
Here is an example '.resin-sync.yml' :
$ cat $PWD/.resin-sync.yml
destination: '/usr/src/app'
before: 'echo Hello'
after: 'echo Done'
ignore:
- .git
- node_modules/
local_resinos:
app-name: local-app
build-triggers:
- Dockerfile: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
- package.json: file-hash-abcdefabcdefabcdefabcdefabcdefabcdef
environment:
- MY_VARIABLE=123
Command line options have precedence over the ones saved in '.resin-sync.yml'.

View File

@ -60,10 +60,11 @@ module.exports =
Promise = require('bluebird')
_ = require('lodash')
prettyjson = require('prettyjson')
Docker = require('docker-toolbelt')
{ discover } = require('resin-sync')
{ SpinnerPromise } = require('resin-cli-visuals')
{ dockerPort, dockerTimeout } = require('./common')
dockerUtils = require('../../utils/docker')
{ exitWithExpectedError } = require('../../utils/patterns')
if options.timeout?
options.timeout *= 1000
@ -75,15 +76,15 @@ module.exports =
stopMessage: 'Reporting scan results'
.filter ({ address }) ->
Promise.try ->
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
docker.pingAsync()
.return(true)
.catchReturn(false)
.tap (devices) ->
if _.isEmpty(devices)
throw new Error('Could not find any resinOS devices in the local network')
exitWithExpectedError('Could not find any resinOS devices in the local network')
.map ({ host, address }) ->
docker = new Docker(host: address, port: dockerPort, timeout: dockerTimeout)
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
Promise.props
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')

View File

@ -67,10 +67,12 @@ module.exports =
Promise = require 'bluebird'
_ = require('lodash')
{ forms } = require('resin-sync')
{ selectContainerFromDevice, getSubShellCommand } = require('./common')
{ exitWithExpectedError } = require('../../utils/patterns')
if (options.host is true and options.container?)
throw new Error('Please pass either --host or --container option')
exitWithExpectedError('Please pass either --host or --container option')
if not options.port?
options.port = 22222

View File

@ -26,10 +26,6 @@ module.exports =
To continuously stream output, and see new logs in real time, use the `--tail` option.
Note that for now you need to provide the whole UUID for this command to work correctly.
This is due to some technical limitations that we plan to address soon.
Examples:
$ resin logs 23c73a1
@ -47,25 +43,19 @@ module.exports =
primary: true
action: (params, options, done) ->
normalizeUuidProp(params)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
moment = require('moment')
printLine = (line) ->
timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)')
console.log("#{timestamp} #{line.message}")
promise = resin.logs.history(params.uuid).each(printLine)
if not options.tail
# PubNub keeps the process alive after a history query.
# Until this is fixed, we force the process to exit.
# This of course prevents this command to be used programatically
return promise.catch(done).finally ->
process.exit(0)
promise.then ->
resin.logs.subscribe(params.uuid).then (logs) ->
if options.tail
resin.logs.subscribe(params.uuid, { count: 100 }).then (logs) ->
logs.on('line', printLine)
logs.on('error', done)
.catch(done)
.catch(done)
else
resin.logs.history(params.uuid)
.each(printLine)
.catch(done)

View File

@ -43,11 +43,13 @@ exports.set =
normalizeUuidProp(options, 'device')
Promise = require('bluebird')
_ = require('lodash')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
{ exitWithExpectedError } = require('../utils/patterns')
Promise.try ->
if _.isEmpty(params.note)
throw new Error('Missing note content')
exitWithExpectedError('Missing note content')
resin.models.device.note(options.device, params.note)
.nodeify(done)

View File

@ -31,7 +31,7 @@ resolveVersion = (deviceType, version) ->
return Promise.resolve(version)
form = require('resin-cli-form')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.os.getSupportedVersions(deviceType)
.then ({ versions, recommended }) ->
@ -57,7 +57,7 @@ exports.versions =
$ resin os versions raspberrypi3
'''
action: (params, options, done) ->
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.models.os.getSupportedVersions(params.type)
.then ({ versions, recommended }) ->
@ -203,17 +203,20 @@ exports.configure =
Use this command to configure a previously downloaded operating system image for
the specific device or for an application generally.
Calling this command without --version is not recommended, and may fail in
future releases if the OS version cannot be inferred.
Note that device api keys are only supported on ResinOS 2.0.3+.
This comand still supports the *deprecated* format where the UUID and optionally device key
This command still supports the *deprecated* format where the UUID and optionally device key
are passed directly on the command line, but the recommended way is to pass either an --app or
--device argument. The deprecated format will be remove in a future release.
Examples:
$ resin os configure ../path/rpi.img --device 7cf02a6
$ resin os configure ../path/rpi.img --device 7cf02a6 --deviceApiKey <existingDeviceKey>
$ resin os configure ../path/rpi.img --app MyApp
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7
$ resin os configure ../path/rpi.img --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>
$ resin os configure ../path/rpi.img --app MyApp --version 2.12.7
'''
permission: 'user'
options: [
@ -221,6 +224,7 @@ exports.configure =
commandOptions.optionalApplication
commandOptions.optionalDevice
commandOptions.optionalDeviceApiKey
commandOptions.optionalOsVersion
{
signature: 'config'
description: 'path to the config JSON file, see `resin os build-config`'
@ -233,7 +237,7 @@ exports.configure =
fs = require('fs')
Promise = require('bluebird')
readFileAsync = Promise.promisify(fs.readFile)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
init = require('resin-device-init')
helpers = require('../utils/helpers')
patterns = require('../utils/patterns')
@ -244,7 +248,7 @@ exports.configure =
options.application
params.uuid
]).length != 1
patterns.expectedError '''
patterns.exitWithExpectedError '''
To configure an image, you must provide exactly one of:
* A device, with --device <uuid>
@ -279,6 +283,8 @@ exports.configure =
.then(JSON.parse)
return buildConfig(params.image, appOrDevice.device_type, options.advanced)
.then (answers) ->
answers.version = options.version
(if configurationResourceType == 'device'
generateDeviceConfig(appOrDevice, deviceApiKey, answers)
else

View File

@ -18,38 +18,60 @@ dockerUtils = require('../utils/docker')
LATEST = 'latest'
allDeviceTypes = undefined
getDeviceTypes = ->
Bluebird = require('bluebird')
if allDeviceTypes != undefined
return Bluebird.resolve(allDeviceTypes)
resin = require('resin-sdk').fromSharedOptions()
resin.models.config.getDeviceTypes()
.tap (dt) ->
allDeviceTypes = dt
getDeviceTypesWithSameArch = (deviceTypeSlug) ->
_ = require('lodash')
getDeviceTypes()
.then (deviceTypes) ->
deviceType = _.find(deviceTypes, slug: deviceTypeSlug)
_(deviceTypes).filter(arch: deviceType.arch).map('slug').value()
getApplicationsWithSuccessfulBuilds = (deviceType) ->
preload = require('resin-preload')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
resin.pine.get
resource: 'my_application'
options:
filter:
device_type: deviceType
build:
$any:
$alias: 'b'
$expr:
b:
status: 'success'
expand: preload.applicationExpandOptions
select: [ 'id', 'app_name', 'device_type', 'commit' ]
orderby: 'app_name asc'
getDeviceTypesWithSameArch(deviceType)
.then (deviceTypes) ->
resin.pine.get
resource: 'my_application'
options:
$filter:
device_type:
$in: deviceTypes
owns__release:
$any:
$alias: 'r'
$expr:
r:
status: 'success'
$expand: preload.applicationExpandOptions
$select: [ 'id', 'app_name', 'device_type', 'commit', 'should_track_latest_release' ]
$orderby: 'app_name asc'
selectApplication = (deviceType) ->
visuals = require('resin-cli-visuals')
form = require('resin-cli-form')
{ expectedError } = require('../utils/patterns')
{ exitWithExpectedError } = require('../utils/patterns')
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and builds.')
applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.')
applicationInfoSpinner.start()
getApplicationsWithSuccessfulBuilds(deviceType)
.then (applications) ->
applicationInfoSpinner.stop()
if applications.length == 0
expectedError("You have no apps with successful builds for a '#{deviceType}' device type.")
exitWithExpectedError("You have no apps with successful releases for a '#{deviceType}' device type.")
form.ask
message: 'Select an application'
type: 'list'
@ -57,25 +79,25 @@ selectApplication = (deviceType) ->
name: app.app_name
value: app
selectApplicationCommit = (builds) ->
selectApplicationCommit = (releases) ->
form = require('resin-cli-form')
{ expectedError } = require('../utils/patterns')
{ exitWithExpectedError } = require('../utils/patterns')
if builds.length == 0
expectedError('This application has no successful builds.')
if releases.length == 0
exitWithExpectedError('This application has no successful releases.')
DEFAULT_CHOICE = { 'name': LATEST, 'value': LATEST }
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
name: "#{build.push_timestamp} - #{build.commit_hash}"
value: build.commit_hash
choices = [ DEFAULT_CHOICE ].concat releases.map (release) ->
name: "#{release.end_timestamp} - #{release.commit}"
value: release.commit
return form.ask
message: 'Select a build'
message: 'Select a release'
type: 'list'
default: LATEST
choices: choices
offerToDisableAutomaticUpdates = (application, commit) ->
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
form = require('resin-cli-form')
if commit == LATEST or not application.should_track_latest_release
@ -84,9 +106,12 @@ offerToDisableAutomaticUpdates = (application, commit) ->
This application is set to automatically update all devices to the latest available version.
This might be unexpected behaviour: with this enabled, the preloaded device will still
download and install the latest build once it is online.
download and install the latest release once it is online.
Do you want to disable automatic updates for this application?
Warning: To re-enable this requires direct api calls,
see https://docs.resin.io/reference/api/resources/device/#set-device-to-release
'''
form.ask
message: message,
@ -109,12 +134,12 @@ module.exports =
please check the README here: https://github.com/resin-io/resin-cli .
Use this command to preload an application to a local disk image (or
Edison zip archive) with a built commit from Resin.io.
This can be used with cloud builds, or images deployed with resin deploy.
Edison zip archive) with a built release from Resin.io.
Examples:
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
$ resin preload resin.img
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
$ resin preload resin.img
'''
permission: 'user'
primary: true
@ -129,7 +154,7 @@ module.exports =
signature: 'commit'
parameter: 'hash'
description: '''
a specific application commit to preload, use "latest" to specify the latest commit
the commit hash for a specific application release to preload, use "latest" to specify the latest release
(ignored if no appId is given)
'''
alias: 'c'
@ -141,20 +166,25 @@ module.exports =
alias: 's'
}
{
signature: 'dont-check-device-type'
signature: 'dont-check-arch'
boolean: true
description: 'Disables check for matching device types in image and application'
description: 'Disables check for matching architecture in image and application'
}
{
signature: 'pin-device-to-release'
boolean: true
description: 'Pin the preloaded device to the preloaded release on provision'
alias: 'p'
}
]
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
preload = require('resin-preload')
errors = require('resin-errors')
visuals = require('resin-cli-visuals')
nodeCleanup = require('node-cleanup')
{ expectedError } = require('../utils/patterns')
{ exitWithExpectedError } = require('../utils/patterns')
progressBars = {}
@ -183,21 +213,28 @@ module.exports =
options.splashImage = options['splash-image']
delete options['splash-image']
if options['dont-check-device-type'] and not options.appId
expectedError('You need to specify an app id if you disable the device type check.')
options.dontCheckArch = options['dont-check-arch'] || false
delete options['dont-check-arch']
if options.dontCheckArch and not options.appId
exitWithExpectedError('You need to specify an app id if you disable the architecture check.')
options.pinDevice = options['pin-device-to-release'] || false
delete options['pin-device-to-release']
# Get a configured dockerode instance
dockerUtils.getDocker(options)
.then (docker) ->
preloader = new preload.Preloader(
resin,
docker,
options.appId,
options.commit,
options.image,
options.splashImage,
options.proxy,
resin
docker
options.appId
options.commit
options.image
options.splashImage
options.proxy
options.dontCheckArch
options.pinDevice
)
gotSignal = false
@ -221,51 +258,39 @@ module.exports =
return new Promise (resolve, reject) ->
preloader.on('error', reject)
preloader.build()
preloader.prepare()
.then ->
preloader.prepare()
.then ->
preloader.getDeviceTypeAndPreloadedBuilds()
.then (info) ->
# If no appId was provided, show a list of matching apps
Promise.try ->
if options.appId
return preloader.fetchApplication()
.catch(errors.ResinApplicationNotFound, expectedError)
selectApplication(info.device_type)
.then (application) ->
preloader.setApplication(application)
# Check that the app device type and the image device type match
if not options['dont-check-device-type'] and info.device_type != application.device_type
expectedError(
"Image device type (#{info.device_type}) and application device type (#{application.device_type}) do not match"
)
if not preloader.appId
selectApplication(preloader.config.deviceType)
.then (application) ->
preloader.setApplication(application)
.then ->
# Use the commit given as --commit or show an interactive commit selection menu
Promise.try ->
if options.commit
if options.commit == LATEST and preloader.application.commit
# handle `--commit latest`
return LATEST
release = _.find preloader.application.owns__release, (release) ->
release.commit.startsWith(options.commit)
if not release
exitWithExpectedError('There is no release matching this commit')
return release.commit
selectApplicationCommit(preloader.application.owns__release)
.then (commit) ->
if commit == LATEST
preloader.commit = preloader.application.commit
else
preloader.commit = commit
# Use the commit given as --commit or show an interactive commit selection menu
Promise.try ->
if options.commit
if options.commit == LATEST and application.commit
# handle `--commit latest`
return LATEST
else if not _.find(application.build, commit_hash: options.commit)
expectedError('There is no build matching this commit')
return options.commit
selectApplicationCommit(application.build)
.then (commit) ->
if commit == LATEST
preloader.commit = application.commit
else
preloader.commit = commit
# Propose to disable automatic app updates if the commit is not the latest
offerToDisableAutomaticUpdates(application, commit)
.then ->
builds = info.preloaded_builds.map (build) ->
build.slice(-preload.BUILD_HASH_LENGTH)
if preloader.commit in builds
throw new preload.errors.ResinError('This build is already preloaded in this image.')
# All options are ready: preload the image.
preloader.preload()
.catch(preload.errors.ResinError, expectedError)
# Propose to disable automatic app updates if the commit is not the latest
offerToDisableAutomaticUpdates(preloader.application, commit)
.then ->
# All options are ready: preload the image.
preloader.preload()
.catch(resin.errors.ResinError, exitWithExpectedError)
.then(resolve)
.catch(reject)
.then(done)

229
lib/actions/push.ts Normal file
View File

@ -0,0 +1,229 @@
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { CommandDefinition } from 'capitano';
import { stripIndent } from 'common-tags';
import { ResinSDK } from 'resin-sdk';
import { BuildError } from '../utils/device/errors';
// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html
const IP_REGEX = new RegExp(
/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/,
);
enum BuildTarget {
Cloud,
Device,
}
function getBuildTarget(appOrDevice: string): BuildTarget | null {
// First try the application regex from the api
if (/^[a-zA-Z0-9_-]+$/.test(appOrDevice)) {
return BuildTarget.Cloud;
}
if (IP_REGEX.test(appOrDevice)) {
return BuildTarget.Device;
}
return null;
}
async function getAppOwner(sdk: ResinSDK, appName: string) {
const {
exitWithExpectedError,
selectFromList,
} = await import('../utils/patterns');
const _ = await import('lodash');
const applications = await sdk.models.application.getAll({
$expand: {
user: {
$select: ['username'],
},
},
$filter: {
app_name: appName,
},
$select: ['id'],
});
if (applications == null || applications.length === 0) {
exitWithExpectedError(
stripIndent`
No applications found with name: ${appName}.
This could mean that the application does not exist, or you do
not have the permissions required to access it.`,
);
}
if (applications.length === 1) {
return _.get(applications, '[0].user[0].username');
}
// If we got more than one application with the same name it means that the
// user has access to a collab app with the same name as a personal app. We
// present a list to the user which shows the fully qualified application
// name (user/appname) and allows them to select
const entries = _.map(applications, app => {
const username = _.get(app, 'user[0].username');
return {
name: `${username}/${appName}`,
extra: username,
};
});
const selected = await selectFromList(
`${
entries.length
} applications found with that name, please select the application you would like to push to`,
entries,
);
return selected.extra;
}
export const push: CommandDefinition<
{
applicationOrDevice: string;
},
{
source: string;
emulated: boolean;
nocache: boolean;
}
> = {
signature: 'push <applicationOrDevice>',
description:
'Start a remote build on the resin.io cloud build servers or a local mode device',
help: stripIndent`
This command can be used to start a build on the remote
resin.io cloud builders, or a local mode resin device.
When building on the resin cloud the given source directory will be sent to the
resin.io builder, and the build will proceed. This can be used as a drop-in
replacement for git push to deploy.
When building on a local mode device, the given source directory will be built on
device, and the resulting containers will be run on the device. Logs will be
streamed back from the device as part of the same invocation.
Examples:
$ resin push myApp
$ resin push myApp --source <source directory>
$ resin push myApp -s <source directory>
$ resin push 10.0.0.1
$ resin push 10.0.0.1 --source <source directory>
$ resin push 10.0.0.1 -s <source directory>
`,
permission: 'user',
options: [
{
signature: 'source',
alias: 's',
description:
'The source that should be sent to the resin builder to be built (defaults to the current directory)',
parameter: 'source',
},
{
signature: 'emulated',
alias: 'e',
description: 'Force an emulated build to occur on the remote builder',
boolean: true,
},
{
signature: 'nocache',
alias: 'c',
description: "Don't use cache when building this project",
boolean: true,
},
],
async action(params, options, done) {
const sdk = (await import('resin-sdk')).fromSharedOptions();
const Bluebird = await import('bluebird');
const remote = await import('../utils/remote-build');
const deviceDeploy = await import('../utils/device/deploy');
const { exitWithExpectedError } = await import('../utils/patterns');
const appOrDevice: string | null = params.applicationOrDevice;
if (appOrDevice == null) {
exitWithExpectedError('You must specify an application or a device');
}
const source = options.source || '.';
if (process.env.DEBUG) {
console.log(`[debug] Using ${source} as build source`);
}
const buildTarget = getBuildTarget(appOrDevice);
switch (buildTarget) {
case BuildTarget.Cloud:
const app = appOrDevice;
Bluebird.join(
sdk.auth.getToken(),
sdk.settings.get('resinUrl'),
getAppOwner(sdk, app),
(token, baseUrl, owner) => {
const opts = {
emulated: options.emulated,
nocache: options.nocache,
};
const args = {
app,
owner,
source,
auth: token,
baseUrl,
sdk,
opts,
};
return remote.startRemoteBuild(args);
},
).nodeify(done);
break;
case BuildTarget.Device:
const device = appOrDevice;
// TODO: Support passing a different port
Bluebird.resolve(
deviceDeploy.deployToDevice({
source,
deviceHost: device,
}),
)
.catch(BuildError, e => {
exitWithExpectedError(e.toString());
})
.nodeify(done);
break;
default:
exitWithExpectedError(
stripIndent`
Build target not recognised. Please provide either an application name or device address.
The only supported device addresses currently are IP addresses.
If you believe your build target should have been detected, and this is an error, please
create an issue.`,
);
break;
}
},
};

View File

@ -108,7 +108,7 @@ module.exports =
console.info("Connecting to: #{uuid}")
resin.models.device.get(uuid)
.then (device) ->
throw new Error('Device is not online') if not device.is_online
patterns.exitWithExpectedError('Device is not online') if not device.is_online
Promise.props
username: resin.auth.whoami()

View File

@ -34,29 +34,28 @@ exports.wizard =
'''
primary: true
action: (params, options, done) ->
Promise = require('bluebird')
capitanoRunAsync = Promise.promisify(require('capitano').run)
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
patterns = require('../utils/patterns')
{ runCommand } = require('../utils/helpers')
resin.auth.isLoggedIn().then (isLoggedIn) ->
return if isLoggedIn
console.info('Looks like you\'re not logged in yet!')
console.info('Lets go through a quick wizard to get you started.\n')
return capitanoRunAsync('login')
console.info("Let's go through a quick wizard to get you started.\n")
return runCommand('login')
.then ->
return if params.name?
patterns.selectOrCreateApplication().tap (applicationName) ->
resin.models.application.has(applicationName).then (hasApplication) ->
return applicationName if hasApplication
capitanoRunAsync("app create #{applicationName}")
runCommand("app create #{applicationName}")
.then (applicationName) ->
params.name = applicationName
.then ->
return capitanoRunAsync("device init --application #{params.name}")
return runCommand("device init --application #{params.name}")
.tap(patterns.awaitDevice)
.then (uuid) ->
return capitanoRunAsync("device #{uuid}")
return runCommand("device #{uuid}")
.then ->
return resin.models.application.get(params.name)
.then (application) ->

View File

@ -17,7 +17,8 @@ limitations under the License.
Raven = require('raven')
Raven.disableConsoleAlerts()
Raven.config require('./config').sentryDsn,
captureUnhandledRejections: true
captureUnhandledRejections: true,
autoBreadcrumbs: true,
release: require('../package.json').version
.install (logged, error) ->
console.error(error)
@ -62,20 +63,21 @@ capitanoExecuteAsync = Promise.promisify(capitano.execute)
# We don't yet use resin-sdk directly everywhere, but we set up shared
# options correctly so we can do safely in submodules
require('resin-sdk').setSharedOptions(
ResinSdk = require('resin-sdk')
ResinSdk.setSharedOptions(
apiUrl: settings.get('apiUrl')
imageMakerUrl: settings.get('imageMakerUrl')
dataDirectory: settings.get('dataDirectory')
retries: 2
)
# Keep using sdk-preconfigured for now, but only temporarily
resin = require('resin-sdk-preconfigured')
resin = ResinSdk.fromSharedOptions()
actions = require('./actions')
errors = require('./errors')
events = require('./events')
plugins = require('./utils/plugins')
update = require('./utils/update')
{ exitWithExpectedError } = require('./utils/patterns')
# Assign bluebird as the global promise library
# stream-to-promise will produce native promises if not
@ -86,13 +88,13 @@ require('any-promise/register/bluebird')
capitano.permission 'user', (done) ->
resin.auth.isLoggedIn().then (isLoggedIn) ->
if not isLoggedIn
throw new Error '''
exitWithExpectedError('''
You have to log in to continue
Run the following command to go through the login wizard:
$ resin login
'''
''')
.nodeify(done)
capitano.command
@ -114,11 +116,8 @@ capitano.command(actions.help.help)
# ---------- Wizard Module ----------
capitano.command(actions.wizard.wizard)
# ---------- Auth Module ----------
capitano.command(actions.auth.login)
capitano.command(actions.auth.logout)
capitano.command(actions.auth.signup)
capitano.command(actions.auth.whoami)
# ---------- Api key module ----------
capitano.command(actions.apiKey.generate)
# ---------- App Module ----------
capitano.command(actions.app.create)
@ -127,6 +126,12 @@ capitano.command(actions.app.remove)
capitano.command(actions.app.restart)
capitano.command(actions.app.info)
# ---------- Auth Module ----------
capitano.command(actions.auth.login)
capitano.command(actions.auth.logout)
capitano.command(actions.auth.signup)
capitano.command(actions.auth.whoami)
# ---------- Device Module ----------
capitano.command(actions.device.list)
capitano.command(actions.device.supported)
@ -202,22 +207,28 @@ capitano.command(actions.util.availableDrives)
# ---------- Internal utils ----------
capitano.command(actions.internal.osInit)
capitano.command(actions.internal.scanDevices)
capitano.command(actions.internal.sudo)
#------------ Local build and deploy -------
capitano.command(actions.build)
capitano.command(actions.deploy)
#------------ Push/remote builds -------
capitano.command(actions.push.push)
#------------ Join/Leave -------
capitano.command(actions.join.join)
capitano.command(actions.leave.leave)
update.notify()
plugins.register(/^resin-plugin-(.+)$/).then ->
cli = capitano.parse(process.argv)
runCommand = ->
if cli.global?.help
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
else
capitanoExecuteAsync(cli)
Promise.all([events.trackCommand(cli), runCommand()])
cli = capitano.parse(process.argv)
runCommand = ->
if cli.global?.help
capitanoExecuteAsync(command: "help #{cli.command ? ''}")
else
capitanoExecuteAsync(cli)
Promise.all([events.trackCommand(cli), runCommand()])
.catch(errors.handle)

View File

@ -19,7 +19,7 @@ limitations under the License.
###
open = require('opn')
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
server = require('./server')
utils = require('./utils')
@ -56,7 +56,7 @@ exports.login = ->
# Leave a bit of time for the
# server to get up and runing
setTimeout ->
open(loginUrl)
open(loginUrl, { wait: false })
, 1000
return server.awaitForToken(options)

View File

@ -77,9 +77,9 @@ exports.awaitForToken = (options) ->
Promise.try ->
if not token
throw new Error('No token')
return utils.isTokenValid(token)
.tap (isValid) ->
if not isValid
return utils.loginIfTokenValid(token)
.tap (loggedIn) ->
if not loggedIn
throw new Error('Invalid token')
.then ->
renderAndDone({ request, response, viewName: 'success', token })

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
###
resin = require('resin-sdk-preconfigured')
resin = require('resin-sdk').fromSharedOptions()
_ = require('lodash')
url = require('url')
Promise = require('bluebird')
@ -42,7 +42,7 @@ exports.getDashboardLoginURL = (callbackUrl) ->
return url.resolve(dashboardUrl, "/login/cli/#{callbackUrl}")
###*
# @summary Check if a token is valid
# @summary Log in using a token, but only if the token is valid
# @function
# @protected
#
@ -50,21 +50,26 @@ exports.getDashboardLoginURL = (callbackUrl) ->
# This function checks that the token is not only well-structured
# but that it also authenticates with the server successfully.
#
# @param {String} sessionToken - token
# @fulfil {Boolean} - whether is valid or not
# If authenticated, the token is persisted, if not then the previous
# login state is restored.
#
# @param {String} token - session token or api key
# @fulfil {Boolean} - whether the login was successful or not
# @returns {Promise}
#
# utils.isTokenValid('...').then (isValid) ->
# if isValid
# utils.loginIfTokenValid('...').then (loggedIn) ->
# if loggedIn
# console.log('Token is valid!')
###
exports.isTokenValid = (sessionToken) ->
if not sessionToken? or _.isEmpty(sessionToken.trim())
exports.loginIfTokenValid = (token) ->
if not token? or _.isEmpty(token.trim())
return Promise.resolve(false)
return resin.token.get().then (currentToken) ->
resin.auth.loginWithToken(sessionToken)
.return(sessionToken)
return resin.auth.getToken()
.catchReturn(undefined)
.then (currentToken) ->
resin.auth.loginWithToken(token)
.return(token)
.then(resin.auth.isLoggedIn)
.tap (isLoggedIn) ->
return if isLoggedIn

View File

@ -14,18 +14,98 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import errors = require('resin-cli-errors');
import patterns = require('./utils/patterns');
import Raven = require('raven');
import Promise = require('bluebird');
import * as _ from 'lodash';
import * as os from 'os';
import * as Raven from 'raven';
import * as Promise from 'bluebird';
import { stripIndent } from 'common-tags';
import * as patterns from './utils/patterns';
const captureException = Promise.promisify<string, Error>(
Raven.captureException,
{ context: Raven },
);
function hasCode(error: any): error is Error & { code: string } {
return error.code != null;
}
function treatFailedBindingAsMissingModule(error: any): void {
if (error.message.startsWith('Could not locate the bindings file.')) {
error.code = 'MODULE_NOT_FOUND';
}
}
function interpret(error: any): string | undefined {
if (!(error instanceof Error)) {
return;
}
treatFailedBindingAsMissingModule(error);
if (hasCode(error)) {
const errorCodeHandler = messages[error.code];
const message = errorCodeHandler && errorCodeHandler(error);
if (message) {
return message;
}
if (!_.isEmpty(error.message)) {
return `${error.code}: ${error.message}`;
}
} else {
return error.message;
}
}
const messages: {
[key: string]: (error: Error & { path?: string }) => string;
} = {
EISDIR: error => `File is a directory: ${error.path}`,
ENOENT: error => `No such file or directory: ${error.path}`,
ENOGIT: () => stripIndent`
Git is not installed on this system.
Head over to http://git-scm.com to install it and run this command again.`,
EPERM: () => stripIndent`
You don't have enough privileges to run this operation.
${
os.platform() === 'win32'
? 'Run a new Command Prompt as administrator and try running this command again.'
: 'Try running this command again prefixing it with `sudo`.'
}
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
EACCES: e => messages.EPERM(e),
ETIMEDOUT: () =>
'Oops something went wrong, please check your connection and try again.',
MODULE_NOT_FOUND: () => stripIndent`
Part of the CLI could not be loaded. This typically means your CLI install is in a broken state.
${
os.arch() === 'x64'
? 'You can normally fix this by uninstalling and reinstalling the CLI.'
: stripIndent`
You're using an unsupported architecture (${os.arch()}), so this is typically caused by missing native modules.
Reinstalling may help, but pay attention to errors in native module build steps en route.
`
}
`,
ResinExpiredToken: () => stripIndent`
Looks like your session token is expired.
Please try logging in again with:
$ resin login`,
};
exports.handle = function(error: any) {
let message = errors.interpret(error);
let message = interpret(error);
if (message == null) {
return;
}
@ -34,7 +114,7 @@ exports.handle = function(error: any) {
message = error.stack;
}
patterns.printErrorMessage(message);
patterns.printErrorMessage(message!);
return captureException(error)
.timeout(1000)

747
lib/utils/compose.coffee Normal file
View File

@ -0,0 +1,747 @@
Promise = require('bluebird')
path = require('path')
exports.appendProjectOptions = appendProjectOptions = (opts) ->
opts.concat [
{
signature: 'projectName'
parameter: 'projectName'
description: 'Specify an alternate project name; default is the directory name'
alias: 'n'
},
]
exports.appendOptions = (opts) ->
appendProjectOptions(opts).concat [
{
signature: 'emulated'
description: 'Run an emulated build using Qemu'
boolean: true
alias: 'e'
},
{
signature: 'logs'
description: 'Display full log output'
boolean: true
},
]
exports.generateOpts = (options) ->
fs = require('mz/fs')
fs.realpath(options.source || '.').then (projectPath) ->
projectName: options.projectName
projectPath: projectPath
inlineLogs: !!options.logs
compositionFileNames = [
'docker-compose.yml'
'docker-compose.yaml'
]
# look into the given directory for valid compose files and return
# the contents of the first one found.
exports.resolveProject = resolveProject = (rootDir) ->
fs = require('mz/fs')
Promise.any compositionFileNames.map (filename) ->
fs.readFile(path.join(rootDir, filename), 'utf-8')
# Parse the given composition and return a structure with info. Input is:
# - composePath: the *absolute* path to the directory containing the compose file
# - composeStr: the contents of the compose file, as a string
createProject = (composePath, composeStr, projectName = null) ->
yml = require('js-yaml')
compose = require('resin-compose-parse')
# both methods below may throw.
composition = yml.safeLoad(composeStr, schema: yml.FAILSAFE_SCHEMA)
composition = compose.normalize(composition)
projectName ?= path.basename(composePath)
descriptors = compose.parse(composition).map (descr) ->
# generate an image name based on the project and service names
# if one is not given and the service requires a build
if descr.image.context? and not descr.image.tag?
descr.image.tag = [ projectName, descr.serviceName ].join('_').toLowerCase()
return descr
return {
path: composePath,
name: projectName,
composition,
descriptors
}
# high-level function resolving a project and creating a composition out
# of it in one go. if image is given, it'll create a default project for
# that without looking for a project. falls back to creating a default
# project if none is found at the given projectPath.
exports.loadProject = (logger, projectPath, projectName, image) ->
compose = require('resin-compose-parse')
logger.logDebug('Loading project...')
Promise.try ->
if image?
logger.logInfo("Creating default composition with image: #{image}")
return compose.defaultComposition(image)
logger.logDebug('Resolving project...')
resolveProject(projectPath)
.tap ->
logger.logInfo('Compose file detected')
.catch (e) ->
logger.logDebug("Failed to resolve project: #{e}")
logger.logInfo("Creating default composition with source: #{projectPath}")
return compose.defaultComposition()
.then (composeStr) ->
logger.logDebug('Creating project...')
createProject(projectPath, composeStr, projectName)
toPosixPath = (systemPath) ->
path = require('path')
systemPath.replace(new RegExp('\\' + path.sep, 'g'), '/')
exports.tarDirectory = tarDirectory = (dir) ->
tar = require('tar-stream')
klaw = require('klaw')
path = require('path')
fs = require('mz/fs')
streamToPromise = require('stream-to-promise')
{ FileIgnorer } = require('./ignore')
getFiles = ->
streamToPromise(klaw(dir))
.filter((item) -> not item.stats.isDirectory())
.map((item) -> item.path)
ignore = new FileIgnorer(dir)
pack = tar.pack()
getFiles(dir)
.each (file) ->
type = ignore.getIgnoreFileType(path.relative(dir, file))
if type?
ignore.addIgnoreFile(file, type)
.filter(ignore.filter)
.map (file) ->
relPath = path.relative(path.resolve(dir), file)
Promise.join relPath, fs.stat(file), fs.readFile(file),
(filename, stats, data) ->
pack.entry({ name: toPosixPath(filename), size: stats.size, mode: stats.mode }, data)
.then ->
pack.finalize()
return pack
truncateString = (str, len) ->
return str if str.length < len
str = str.slice(0, len)
# return everything up to the last line. this is a cheeky way to avoid
# having to deal with splitting the string midway through some special
# character sequence.
return str.slice(0, str.lastIndexOf('\n'))
LOG_LENGTH_MAX = 512 * 1024 # 512KB
exports.buildProject = (
docker, logger,
projectPath, projectName, composition,
arch, deviceType,
emulated, buildOpts,
inlineLogs
) ->
_ = require('lodash')
humanize = require('humanize')
compose = require('resin-compose-parse')
builder = require('resin-multibuild')
transpose = require('docker-qemu-transpose')
qemu = require('./qemu')
logger.logInfo("Building for #{arch}/#{deviceType}")
imageDescriptors = compose.parse(composition)
imageDescriptorsByServiceName = _.keyBy(imageDescriptors, 'serviceName')
if inlineLogs
renderer = new BuildProgressInline(logger.streams['build'], imageDescriptors)
else
tty = require('./tty')(process.stdout)
renderer = new BuildProgressUI(tty, imageDescriptors)
renderer.start()
qemu.installQemuIfNeeded(emulated, logger)
.tap (needsQemu) ->
return if not needsQemu
logger.logInfo('Emulation is enabled')
# Copy qemu into all build contexts
Promise.map imageDescriptors, (d) ->
return if not d.image.context? # external image
return qemu.copyQemu(path.join(projectPath, d.image.context))
.then (needsQemu) ->
# Tar up the directory, ready for the build stream
tarDirectory(projectPath)
.then (tarStream) ->
builder.splitBuildStream(composition, tarStream)
.tap (tasks) ->
# Updates each task as a side-effect
builder.performResolution(tasks, arch, deviceType)
.map (task) ->
if not task.external and not task.resolved
throw new Error(
"Project type for service '#{task.serviceName}' could not be determined. " +
'Please add a Dockerfile'
)
.map (task) ->
d = imageDescriptorsByServiceName[task.serviceName]
# multibuild parses the composition internally so any tags we've
# set before are lost; re-assign them here
task.tag ?= [ projectName, task.serviceName ].join('_').toLowerCase()
if d.image.context?
d.image.tag = task.tag
# configure build opts appropriately
task.dockerOpts ?= {}
_.merge(task.dockerOpts, buildOpts, { t: task.tag })
if d.image.context?.args?
task.dockerOpts.buildargs ?= {}
_.merge(task.dockerOpts.buildargs, d.image.context.args)
# Get the service-specific log stream
# Caveat: `multibuild.BuildTask` defines no `logStream` property
# but it's convenient to store it there; it's JS ultimately.
task.logStream = renderer.streams[task.serviceName]
task.logBuffer = []
# Setup emulation if needed
return [ task, null ] if task.external or not needsQemu
binPath = qemu.qemuPathInContext(path.join(projectPath, task.context))
transpose.transposeTarStream task.buildStream,
hostQemuPath: toPosixPath(binPath)
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
.then (stream) ->
task.buildStream = stream
.return([ task, binPath ])
.map ([ task, qemuPath ]) ->
Promise.resolve(task).tap (task) ->
captureStream = buildLogCapture(task.external, task.logBuffer)
if task.external
# External image -- there's no build to be performed,
# just follow pull progress.
captureStream.pipe(task.logStream)
task.progressHook = pullProgressAdapter(captureStream)
else
task.streamHook = (stream) ->
stream = createLogStream(stream)
if qemuPath?
buildThroughStream = transpose.getBuildThroughStream
hostQemuPath: toPosixPath(qemuPath)
containerQemuPath: "/tmp/#{qemu.QEMU_BIN_NAME}"
rawStream = stream.pipe(buildThroughStream)
else
rawStream = stream
# `stream` sends out raw strings in contrast to `task.progressHook`
# where we're given objects. capture these strings as they come
# before we parse them.
rawStream
.pipe(dropEmptyLinesStream())
.pipe(captureStream)
.pipe(buildProgressAdapter(inlineLogs))
.pipe(task.logStream)
.then (tasks) ->
logger.logDebug 'Prepared tasks; building...'
builder.performBuilds(tasks, docker)
.map (builtImage) ->
if not builtImage.successful
builtImage.error.serviceName = builtImage.serviceName
throw builtImage.error
d = imageDescriptorsByServiceName[builtImage.serviceName]
task = _.find(tasks, serviceName: builtImage.serviceName)
image =
serviceName: d.serviceName
name: d.image.tag ? d.image
logs: truncateString(task.logBuffer.join('\n'), LOG_LENGTH_MAX)
props:
dockerfile: builtImage.dockerfile
projectType: builtImage.projectType
# Times here are timestamps, so test whether they're null
# before creating a date out of them, as `new Date(null)`
# creates a date representing UNIX time 0.
if (startTime = builtImage.startTime)
image.props.startTime = new Date(startTime)
if (endTime = builtImage.endTime)
image.props.endTime = new Date(endTime)
docker.getImage(image.name).inspect().get('Size').then (size) ->
image.props.size = size
.return(image)
.tap (images) ->
summary = _(images).map ({ serviceName, props }) ->
[ serviceName, "Image size: #{humanize.filesize(props.size)}" ]
.fromPairs()
.value()
renderer.end(summary)
.finally(renderer.end)
createRelease = (apiEndpoint, auth, userId, appId, composition) ->
_ = require('lodash')
crypto = require('crypto')
releaseMod = require('resin-release')
client = releaseMod.createClient({ apiEndpoint, auth })
releaseMod.create
client: client
user: userId
application: appId
composition: composition
source: 'local'
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase()
.then ({ release, serviceImages }) ->
release = _.omit(release, [
'created_at'
'belongs_to__application'
'is_created_by__user'
'__metadata'
])
_.keys serviceImages, (serviceName) ->
serviceImages[serviceName] = _.omit(serviceImages[serviceName], [
'created_at'
'is_a_build_of__service'
'__metadata'
])
return { client, release, serviceImages }
tagServiceImages = (docker, images, serviceImages) ->
Promise.map images, (d) ->
serviceImage = serviceImages[d.serviceName]
imageName = serviceImage.is_stored_at__image_location
[ _match, registry, repo, tag = 'latest' ] = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName)
name = "#{registry}/#{repo}"
docker.getImage(d.name).tag({ repo: name, tag, force: true })
.then ->
docker.getImage("#{name}:#{tag}")
.then (localImage) ->
serviceName: d.serviceName
serviceImage: serviceImage
localImage: localImage
registry: registry
repo: repo
logs: d.logs
props: d.props
authorizePush = (tokenAuthEndpoint, registry, images) ->
_ = require('lodash')
sdk = require('resin-sdk').fromSharedOptions()
if not _.isArray(images)
images = [ images ]
sdk.request.send
baseUrl: tokenAuthEndpoint
url: '/auth/v1/token'
qs:
service: registry
scope: images.map (repo) ->
"repository:#{repo}:pull,push"
.get('body')
.get('token')
.catchReturn({})
pushAndUpdateServiceImages = (docker, token, images, afterEach) ->
chalk = require('chalk')
{ DockerProgress } = require('docker-progress')
tty = require('./tty')(process.stdout)
opts = { authconfig: registrytoken: token }
progress = new DockerProgress(dockerToolbelt: docker)
renderer = pushProgressRenderer(tty, chalk.blue('[Push]') + ' ')
reporters = progress.aggregateProgress(images.length, renderer)
Promise.using tty.cursorHidden(), ->
Promise.map images, ({ serviceImage, localImage, props, logs }, index) ->
Promise.join(
localImage.inspect().get('Size')
progress.push(localImage.name, reporters[index], opts).finally(renderer.end)
(size, digest) ->
serviceImage.image_size = size
serviceImage.content_hash = digest
serviceImage.build_log = logs
serviceImage.dockerfile = props.dockerfile
serviceImage.project_type = props.projectType
serviceImage.start_timestamp = props.startTime if props.startTime
serviceImage.end_timestamp = props.endTime if props.endTime
serviceImage.push_timestamp = new Date()
serviceImage.status = 'success'
)
.tapCatch (e) ->
serviceImage.error_message = '' + e
serviceImage.status = 'failed'
.finally ->
afterEach?(serviceImage, props)
exports.deployProject = (
docker, logger,
composition, images,
appId, userId, auth,
apiEndpoint,
skipLogUpload
) ->
_ = require('lodash')
chalk = require('chalk')
releaseMod = require('resin-release')
tty = require('./tty')(process.stdout)
prefix = chalk.cyan('[Info]') + ' '
spinner = createSpinner()
runloop = runSpinner(tty, spinner, "#{prefix}Creating release...")
createRelease(apiEndpoint, auth, userId, appId, composition)
.finally(runloop.end)
.then ({ client, release, serviceImages }) ->
logger.logDebug('Tagging images...')
tagServiceImages(docker, images, serviceImages)
.tap (images) ->
logger.logDebug('Authorizing push...')
authorizePush(apiEndpoint, images[0].registry, _.map(images, 'repo'))
.then (token) ->
logger.logInfo('Pushing images to registry...')
pushAndUpdateServiceImages docker, token, images, (serviceImage) ->
logger.logDebug("Saving image #{serviceImage.is_stored_at__image_location}")
if skipLogUpload
delete serviceImage.build_log
releaseMod.updateImage(client, serviceImage.id, serviceImage)
.finally ->
logger.logDebug('Untagging images...')
Promise.map images, ({ localImage }) ->
localImage.remove()
.then ->
release.status = 'success'
.tapCatch (e) ->
release.status = 'failed'
.finally ->
runloop = runSpinner(tty, spinner, "#{prefix}Saving release...")
release.end_timestamp = new Date()
releaseMod.updateRelease(client, release.id, release)
.finally(runloop.end)
.return(release)
# utilities
renderProgressBar = (percentage, stepCount) ->
_ = require('lodash')
percentage = _.clamp(percentage, 0, 100)
barCount = stepCount * percentage // 100
spaceCount = stepCount - barCount
bar = "[#{_.repeat('=', barCount)}>#{_.repeat(' ', spaceCount)}]"
return "#{bar} #{_.padStart(percentage, 3)}%"
pushProgressRenderer = (tty, prefix) ->
fn = (e) ->
{ error, percentage } = e
throw new Error(error) if error?
bar = renderProgressBar(percentage, 40)
tty.replaceLine("#{prefix}#{bar}\r")
fn.end = ->
tty.clearLine()
return fn
createLogStream = (input) ->
split = require('split')
stripAnsi = require('strip-ansi-stream')
return input.pipe(stripAnsi()).pipe(split())
dropEmptyLinesStream = ->
through = require('through2')
through (data, enc, cb) ->
str = data.toString('utf-8')
@push(str) if str.trim()
cb()
buildLogCapture = (objectMode, buffer) ->
through = require('through2')
through { objectMode }, (data, enc, cb) ->
# data from pull stream
if data.error
buffer.push("#{data.error}")
else if data.progress and data.status
buffer.push("#{data.progress}% #{data.status}")
else if data.status
buffer.push("#{data.status}")
# data from build stream
else
buffer.push(data)
cb(null, data)
buildProgressAdapter = (inline) ->
through = require('through2')
stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/
[ step, numSteps, progress ] = [ null, null, undefined ]
through { objectMode: true }, (str, enc, cb) ->
return cb(null, str) if not str?
if inline
return cb(null, { status: str })
if /^Successfully tagged /.test(str)
progress = undefined
else
if (match = stepRegex.exec(str))
step = match[1]
numSteps ?= match[2]
str = match[3]
if step?
str = "Step #{step}/#{numSteps}: #{str}"
progress = parseInt(step, 10) * 100 // parseInt(numSteps, 10)
cb(null, { status: str, progress })
pullProgressAdapter = (outStream) ->
return ({ status, id, percentage, error, errorDetail }) ->
if status?
status = status.replace(/^Status: /, '')
if id?
status = "#{id}: #{status}"
if percentage is 100
percentage = undefined
outStream.write
status: status
progress: percentage
error: errorDetail?.message ? error
createSpinner = ->
chars = '|/-\\'
index = 0
-> chars[(index++) % chars.length]
runSpinner = (tty, spinner, msg) ->
runloop = createRunLoop ->
tty.clearLine()
tty.writeLine("#{msg} #{spinner()}")
tty.cursorUp()
runloop.onEnd = ->
tty.clearLine()
tty.writeLine(msg)
return runloop
createRunLoop = (tick) ->
timerId = setInterval(tick, 1000 / 10)
runloop = {
onEnd: ->
end: ->
clearInterval(timerId)
runloop.onEnd()
}
return runloop
class BuildProgressUI
constructor: (tty, descriptors) ->
_ = require('lodash')
chalk = require('chalk')
through = require('through2')
eventHandler = @_handleEvent
services = _.map(descriptors, 'serviceName')
streams = _(services).map (service) ->
stream = through.obj (event, _enc, cb) ->
eventHandler(service, event)
cb()
stream.pipe(tty.stream)
[ service, stream ]
.fromPairs()
.value()
@_tty = tty
@_serviceToDataMap = {}
@_services = services
# Logger magically prefixes the log line with [Build] etc., but it doesn't
# work well with the spinner we're also showing. Manually build the prefix
# here and bypass the logger.
prefix = chalk.blue('[Build]') + ' '
offset = 10 # account for escape sequences inserted for colouring
@_prefixWidth = offset + prefix.length + _.max(_.map(services, 'length'))
@_prefix = prefix
# these are to handle window wrapping
@_maxLineWidth = null
@_lineWidths = []
@_startTime = null
@_ended = false
@_cancelled = false
@_spinner = createSpinner()
@streams = streams
_handleEvent: (service, event) =>
@_serviceToDataMap[service] = event
_handleInterrupt: =>
@_cancelled = true
@end()
process.exit(130) # 128 + SIGINT
start: =>
process.on('SIGINT', @_handleInterrupt)
@_tty.hideCursor()
@_services.forEach (service) =>
@streams[service].write({ status: 'Preparing...' })
@_runloop = createRunLoop(@_display)
@_startTime = Date.now()
end: (summary = null) =>
return if @_ended
@_ended = true
process.removeListener('SIGINT', @_handleInterrupt)
@_runloop.end()
@_runloop = null
@_clear()
@_renderStatus(true)
@_renderSummary(summary ? @_getServiceSummary())
@_tty.showCursor()
_display: =>
@_clear()
@_renderStatus()
@_renderSummary(@_getServiceSummary())
@_tty.cursorUp(@_services.length + 1) # for status line
_clear: ->
@_tty.deleteToEnd()
@_maxLineWidth = @_tty.currentWindowSize().width
_getServiceSummary: ->
_ = require('lodash')
services = @_services
serviceToDataMap = @_serviceToDataMap
_(services).map (service) ->
{ status, progress, error } = serviceToDataMap[service] ? {}
if error
return "#{error}"
else if progress
bar = renderProgressBar(progress, 20)
return "#{bar} #{status}" if status
return "#{bar}"
else if status
return "#{status}"
else
return 'Waiting...'
.map (data, index) ->
[ services[index], data ]
.fromPairs()
.value()
_renderStatus: (end = false) ->
moment = require('moment')
require('moment-duration-format')(moment)
@_tty.clearLine()
@_tty.write(@_prefix)
if end and @_cancelled
@_tty.writeLine('Build cancelled')
else if end
serviceCount = @_services.length
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
runTime = Date.now() - @_startTime
durationStr = moment.duration(runTime // 1000, 'seconds').format()
@_tty.writeLine("Built #{serviceStr} in #{durationStr}")
else
@_tty.writeLine("Building services... #{@_spinner()}")
_renderSummary: (serviceToStrMap) ->
_ = require('lodash')
chalk = require('chalk')
truncate = require('cli-truncate')
strlen = require('string-width')
@_services.forEach (service, index) =>
str = _.padEnd(@_prefix + chalk.bold(service), @_prefixWidth)
str += serviceToStrMap[service]
if @_maxLineWidth?
str = truncate(str, @_maxLineWidth)
@_lineWidths[index] = strlen(str)
@_tty.clearLine()
@_tty.writeLine(str)
class BuildProgressInline
constructor: (outStream, descriptors) ->
_ = require('lodash')
through = require('through2')
services = _.map(descriptors, 'serviceName')
eventHandler = @_renderEvent
streams = _(services).map (service) ->
stream = through.obj (event, _enc, cb) ->
eventHandler(service, event)
cb()
stream.pipe(outStream)
[ service, stream ]
.fromPairs()
.value()
offset = 10 # account for escape sequences inserted for colouring
@_prefixWidth = offset + _.max(_.map(services, 'length'))
@_outStream = outStream
@_services = services
@_startTime = null
@_ended = false
@streams = streams
start: =>
@_outStream.write('Building services...\n')
@_services.forEach (service) =>
@streams[service].write({ status: 'Preparing...' })
@_startTime = Date.now()
end: (summary = null) =>
moment = require('moment')
require('moment-duration-format')(moment)
return if @_ended
@_ended = true
if summary?
@_services.forEach (service) =>
@_renderEvent(service, summary[service])
if @_cancelled
@_outStream.write('Build cancelled\n')
else
serviceCount = @_services.length
serviceStr = if serviceCount is 1 then '1 service' else "#{serviceCount} services"
runTime = Date.now() - @_startTime
durationStr = moment.duration(runTime // 1000, 'seconds').format()
@_outStream.write("Built #{serviceStr} in #{durationStr}\n")
_renderEvent: (service, event) =>
_ = require('lodash')
chalk = require('chalk')
str = do ->
{ status, error } = event
if error
return "#{error}"
else if status
return "#{status}"
else
return 'Waiting...'
prefix = _.padEnd(chalk.bold(service), @_prefixWidth)
@_outStream.write(prefix)
@_outStream.write(str)
@_outStream.write('\n')

32
lib/utils/compose.d.ts vendored Normal file
View File

@ -0,0 +1,32 @@
import * as Bluebird from 'bluebird';
import * as Stream from 'stream';
import { Composition } from 'resin-compose-parse';
import Logger = require('./logger');
interface Image {
context: string;
tag: string;
}
interface Descriptor {
image: Image | string;
serviceName: string;
}
export function resolveProject(projectRoot: string): Bluebird<string>;
export interface ComposeProject {
path: string;
name: string;
composition: Composition;
descriptors: Descriptor[];
}
export function loadProject(
logger: Logger,
projectPath: string,
projectName: string,
image?: string,
): Bluebird<ComposeProject>;
export function tarDirectory(source: string): Promise<Stream.Readable>;

View File

@ -13,24 +13,37 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as fs from 'fs';
import Promise = require('bluebird');
import ResinSdk = require('resin-sdk');
import _ = require('lodash');
import deviceConfig = require('resin-device-config');
import * as semver from 'resin-semver';
const resin = ResinSdk.fromSharedOptions();
function readRootCa(): Promise<string | void> {
const caFile = process.env.NODE_EXTRA_CA_CERTS;
if (!caFile) {
return Promise.resolve();
}
return Promise.fromCallback(cb =>
fs.readFile(caFile, { encoding: 'utf8' }, cb),
)
.then(pem => Buffer.from(pem).toString('base64'))
.catch({ code: 'ENOENT' }, () => {});
}
export function generateBaseConfig(
application: ResinSdk.Application,
options: {},
options: { version?: string; appUpdatePollInterval?: number },
) {
options = _.mapValues(options, function(value, key) {
if (key === 'appUpdatePollInterval') {
return value * 60 * 1000;
} else {
return value;
}
});
if (options.appUpdatePollInterval) {
options = {
...options,
appUpdatePollInterval: options.appUpdatePollInterval * 60 * 1000,
};
}
return Promise.props({
userId: resin.auth.getUserId(),
@ -40,6 +53,9 @@ export function generateBaseConfig(
registryUrl: resin.settings.get('registryUrl'),
deltaUrl: resin.settings.get('deltaUrl'),
apiConfig: resin.models.config.getAll(),
rootCA: readRootCa().catch(() => {
console.warn('Could not read root CA');
}),
}).then(results => {
return deviceConfig.generate(
{
@ -58,6 +74,7 @@ export function generateBaseConfig(
mixpanel: {
token: results.apiConfig.mixpanelToken,
},
balenaRootCA: results.rootCA,
},
options,
);
@ -66,27 +83,30 @@ export function generateBaseConfig(
export function generateApplicationConfig(
application: ResinSdk.Application,
options: {},
options: { version?: string },
) {
return generateBaseConfig(application, options).tap(config =>
addApplicationKey(config, application.id),
);
return generateBaseConfig(application, options).tap(config => {
if (semver.satisfies(options.version, '>=2.7.8')) {
return addProvisioningKey(config, application.id);
} else {
return addApplicationKey(config, application.id);
}
});
}
export function generateDeviceConfig(
device: ResinSdk.Device & { application_name: string },
deviceApiKey: string | null,
options: {},
device: ResinSdk.Device & { belongs_to__application: ResinSdk.PineDeferred },
deviceApiKey: string | true | null,
options: { version?: string },
) {
return resin.models.application
.get(device.application_name)
.get(device.belongs_to__application.__id)
.then(application => {
return generateBaseConfig(application, options).tap(config => {
// Device API keys are only safe for ResinOS 2.0.3+. We could somehow obtain
// the expected version for this config and generate one when we know it's safe,
// but instead for now we fall back to app keys unless the user has explicitly opted in.
if (deviceApiKey) {
return addDeviceKey(config, device.uuid, deviceApiKey);
} else if (semver.satisfies(options.version, '>=2.0.3')) {
return addDeviceKey(config, device.uuid, true);
} else {
return addApplicationKey(config, application.id);
}
@ -111,9 +131,25 @@ function addApplicationKey(config: any, applicationNameOrId: string | number) {
});
}
function addDeviceKey(config: any, uuid: string, customDeviceApiKey: string) {
function addProvisioningKey(config: any, applicationNameOrId: string | number) {
return resin.models.application
.generateProvisioningKey(applicationNameOrId)
.tap(apiKey => {
config.apiKey = apiKey;
});
}
function addDeviceKey(
config: any,
uuid: string,
customDeviceApiKey: string | true,
) {
return Promise.try(() => {
return customDeviceApiKey || resin.models.device.generateDeviceKey(uuid);
if (customDeviceApiKey === true) {
return resin.models.device.generateDeviceKey(uuid);
} else {
return customDeviceApiKey;
}
}).tap(deviceApiKey => {
config.deviceApiKey = deviceApiKey;
});

View File

@ -0,0 +1,136 @@
Promise = require('bluebird')
getBuilderPushEndpoint = (baseUrl, owner, app) ->
querystring = require('querystring')
args = querystring.stringify({ owner, app })
"https://builder.#{baseUrl}/v1/push?#{args}"
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
querystring = require('querystring')
args = querystring.stringify({ owner, app, buildId })
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
bufferImage = (docker, imageId, bufferFile) ->
Promise = require('bluebird')
streamUtils = require('./streams')
image = docker.getImage(imageId)
imageMetadata = image.inspect()
Promise.join image.get(), imageMetadata.get('Size'), (imageStream, imageSize) ->
streamUtils.buffer(imageStream, bufferFile)
.tap (bufferedStream) ->
bufferedStream.length = imageSize
showPushProgress = (message) ->
visuals = require('resin-cli-visuals')
progressBar = new visuals.Progress(message)
progressBar.update({ percentage: 0 })
return progressBar
uploadToPromise = (uploadRequest, logger) ->
new Promise (resolve, reject) ->
handleMessage = (data) ->
data = data.toString()
logger.logDebug("Received data: #{data}")
try
obj = JSON.parse(data)
catch e
logger.logError('Error parsing reply from remote side')
reject(e)
return
switch obj.type
when 'error' then reject(new Error("Remote error: #{obj.error}"))
when 'success' then resolve(obj)
when 'status' then logger.logInfo(obj.message)
else reject(new Error("Received unexpected reply from remote: #{data}"))
uploadRequest
.on('error', reject)
.on('data', handleMessage)
uploadImage = (imageStream, token, username, url, appName, logger) ->
request = require('request')
progressStream = require('progress-stream')
zlib = require('zlib')
# Need to strip off the newline
progressMessage = logger.formatMessage('info', 'Uploading').slice(0, -1)
progressBar = showPushProgress(progressMessage)
streamWithProgress = imageStream.pipe progressStream
time: 500,
length: imageStream.length
, ({ percentage, eta }) ->
progressBar.update
percentage: Math.min(percentage, 100)
eta: eta
uploadRequest = request.post
url: getBuilderPushEndpoint(url, username, appName)
headers:
'Content-Encoding': 'gzip'
auth:
bearer: token
body: streamWithProgress.pipe(zlib.createGzip({
level: 6
}))
uploadToPromise(uploadRequest, logger)
uploadLogs = (logs, token, url, buildId, username, appName) ->
request = require('request')
request.post
json: true
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
auth:
bearer: token
body: Buffer.from(logs)
###
opts must be a hash with the following keys:
- appName: the name of the app to deploy to
- imageName: the name of the image to deploy
- buildLogs: a string with build output
- shouldUploadLogs
###
module.exports = (docker, logger, token, username, url, opts) ->
tmp = require('tmp')
tmpNameAsync = Promise.promisify(tmp.tmpName)
# Ensure the tmp files gets deleted
tmp.setGracefulCleanup()
{ appName, imageName, buildLogs, shouldUploadLogs } = opts
logs = buildLogs
tmpNameAsync()
.then (bufferFile) ->
logger.logInfo('Initializing deploy...')
bufferImage(docker, imageName, bufferFile)
.then (stream) ->
uploadImage(stream, token, username, url, appName, logger)
.finally ->
# If the file was never written to (for instance because an error
# has occured before any data was written) this call will throw an
# ugly error, just suppress it
Promise.try ->
require('mz/fs').unlink(bufferFile)
.catchReturn()
.tap ({ buildId }) ->
return if not shouldUploadLogs
logger.logInfo('Uploading logs...')
Promise.join(
logs
token
url
buildId
username
appName
uploadLogs
)
.get('buildId')

188
lib/utils/device/api.ts Normal file
View File

@ -0,0 +1,188 @@
import * as Bluebird from 'bluebird';
import * as request from 'request';
import * as Stream from 'stream';
import Logger = require('../logger');
import * as ApiErrors from './errors';
export interface DeviceResponse {
[key: string]: any;
status: 'success' | 'failed';
message?: string;
}
export interface DeviceInfo {
deviceType: string;
arch: string;
}
const deviceEndpoints = {
setTargetState: 'v2/local/target-state',
getTargetState: 'v2/local/target-state',
getDeviceInformation: 'v2/local/device-info',
logs: 'v2/local/logs',
ping: 'ping',
version: 'v2/version',
};
export class DeviceAPI {
private deviceAddress: string;
public constructor(
private logger: Logger,
addr: string,
port: number = 48484,
) {
this.deviceAddress = `http://${addr}:${port}/`;
}
// Either return nothing, or throw an error with the info
public async setTargetState(state: any): Promise<void> {
const url = this.getUrlForAction('setTargetState');
return DeviceAPI.promisifiedRequest(
request.post,
{
url,
json: true,
body: state,
},
this.logger,
);
}
public async getTargetState(): Promise<any> {
const url = this.getUrlForAction('getTargetState');
return DeviceAPI.promisifiedRequest(
request.get,
{
url,
json: true,
},
this.logger,
).then(body => {
return body.state;
});
}
public async getDeviceInformation(): Promise<DeviceInfo> {
const url = this.getUrlForAction('getDeviceInformation');
return DeviceAPI.promisifiedRequest(
request.get,
{
url,
json: true,
},
this.logger,
).then(body => {
return body.info;
});
}
public async ping(): Promise<void> {
const url = this.getUrlForAction('ping');
return DeviceAPI.promisifiedRequest(
request.get,
{
url,
},
this.logger,
);
}
public getVersion(): Promise<string> {
const url = this.getUrlForAction('version');
return DeviceAPI.promisifiedRequest(request.get, {
url,
json: true,
}).then(body => {
if (body.status !== 'success') {
throw new ApiErrors.DeviceAPIError(
'Non-successful response from supervisor version endpoint',
);
}
return body.version;
});
}
public getLogStream(): Bluebird<Stream.Readable> {
const url = this.getUrlForAction('logs');
// Don't use the promisified version here as we want to stream the output
return new Bluebird((resolve, reject) => {
const req = request.get(url);
req.on('error', reject).on('response', res => {
if (res.statusCode !== 200) {
reject(
new ApiErrors.DeviceAPIError(
'Non-200 response from log streaming endpoint',
),
);
}
resolve(res);
});
});
}
private getUrlForAction(action: keyof typeof deviceEndpoints): string {
return `${this.deviceAddress}${deviceEndpoints[action]}`;
}
// A helper method for promisifying general (non-streaming) requests. Streaming
// requests should use a seperate setup
private static async promisifiedRequest<T>(
requestMethod: (
opts: T,
cb: (err?: any, res?: any, body?: any) => void,
) => void,
opts: T,
logger?: Logger,
): Promise<any> {
const Bluebird = await import('bluebird');
const _ = await import('lodash');
type ObjectWithUrl = { url?: string };
if (logger != null) {
let url: string | null = null;
if (_.isObject(opts) && (opts as ObjectWithUrl).url != null) {
// the `as string` shouldn't be necessary, but the type system
// is getting a little confused
url = (opts as ObjectWithUrl).url as string;
} else if (_.isString(opts)) {
url = opts;
}
if (url != null) {
logger.logDebug(`Sending request to ${url}`);
}
}
return Bluebird.fromCallback(
cb => {
return requestMethod(opts, cb);
},
{ multiArgs: true },
).then(([response, body]) => {
switch (response.statusCode) {
case 200:
return body;
case 400:
throw new ApiErrors.BadRequestDeviceAPIError(body.message);
case 503:
throw new ApiErrors.ServiceUnavailableAPIError(body.message);
default:
throw new ApiErrors.DeviceAPIError(body.message);
}
});
}
}
export default DeviceAPI;

306
lib/utils/device/deploy.ts Normal file
View File

@ -0,0 +1,306 @@
import * as Bluebird from 'bluebird';
import * as Docker from 'dockerode';
import * as _ from 'lodash';
import { Composition } from 'resin-compose-parse';
import { BuildTask, LocalImage } from 'resin-multibuild';
import * as semver from 'resin-semver';
import { Readable } from 'stream';
import Logger = require('../logger');
import { displayBuildLog } from './logs';
import { DeviceInfo } from './api';
import * as LocalPushErrors from './errors';
// Define the logger here so the debug output
// can be used everywhere
const logger = new Logger();
export interface DeviceDeployOptions {
source: string;
deviceHost: string;
devicePort?: number;
}
async function checkSource(source: string): Promise<boolean> {
const { fs } = await import('mz');
return (await fs.exists(source)) && (await fs.stat(source)).isDirectory();
}
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
const { loadProject, tarDirectory } = await import('../compose');
const { exitWithExpectedError } = await import('../patterns');
const { DeviceAPI } = await import('./api');
const { displayDeviceLogs } = await import('./logs');
if (!(await checkSource(opts.source))) {
exitWithExpectedError(`Could not access source directory: ${opts.source}`);
}
const api = new DeviceAPI(logger, opts.deviceHost);
// First check that we can access the device with a ping
try {
await api.ping();
} catch (e) {
exitWithExpectedError(
`Could not communicate with local mode device at address ${
opts.deviceHost
}`,
);
}
const versionError = new Error(
'The supervisor version on this remote device does not support multicontainer local mode. ' +
'Please update your device to resinOS v2.20.0 or greater from the dashboard.',
);
try {
const version = await api.getVersion();
if (!semver.satisfies(version, '>=7.21.4')) {
exitWithExpectedError(versionError);
}
} catch {
exitWithExpectedError(versionError);
}
logger.logInfo(`Starting build on device ${opts.deviceHost}`);
const project = await loadProject(logger, opts.source, 'local');
// Attempt to attach to the device's docker daemon
const docker = connectToDocker(
opts.deviceHost,
opts.devicePort != null ? opts.devicePort : 2375,
);
const tarStream = await tarDirectory(opts.source);
// Try to detect the device information
const deviceInfo = await api.getDeviceInformation();
await performBuilds(
project.composition,
tarStream,
docker,
deviceInfo,
logger,
);
logger.logDebug('Setting device state...');
// Now set the target state on the device
const currentTargetState = await api.getTargetState();
const targetState = generateTargetState(
currentTargetState,
project.composition,
);
logger.logDebug(`Sending target state: ${JSON.stringify(targetState)}`);
await api.setTargetState(targetState);
// Print an empty newline to seperate the build output
// from the device output
console.log();
logger.logInfo('Streaming device logs...');
// Now all we need to do is stream back the logs
const logStream = await api.getLogStream();
await displayDeviceLogs(logStream, logger);
}
function connectToDocker(host: string, port: number): Docker {
return new Docker({
host,
port,
Promise: Bluebird as any,
});
}
export async function performBuilds(
composition: Composition,
tarStream: Readable,
docker: Docker,
deviceInfo: DeviceInfo,
logger: Logger,
): Promise<void> {
const multibuild = await import('resin-multibuild');
const buildTasks = await multibuild.splitBuildStream(composition, tarStream);
logger.logDebug('Found build tasks:');
_.each(buildTasks, task => {
let infoStr: string;
if (task.external) {
infoStr = `image pull [${task.imageName}]`;
} else {
infoStr = `build [${task.context}]`;
}
logger.logDebug(` ${task.serviceName}: ${infoStr}`);
});
logger.logDebug(
`Resolving services with [${deviceInfo.deviceType}|${deviceInfo.arch}]`,
);
await multibuild.performResolution(
buildTasks,
deviceInfo.arch,
deviceInfo.deviceType,
);
logger.logDebug('Found project types:');
_.each(buildTasks, task => {
if (!task.external) {
logger.logDebug(` ${task.serviceName}: ${task.projectType}`);
} else {
logger.logDebug(` ${task.serviceName}: External image`);
}
});
logger.logDebug('Probing remote daemon for cache images');
await assignDockerBuildOpts(docker, buildTasks);
logger.logDebug('Starting builds...');
await assignOutputHandlers(buildTasks, logger);
const localImages = await multibuild.performBuilds(buildTasks, docker);
// Check for failures
await inspectBuildResults(localImages);
// Now tag any external images with the correct name that they should be,
// as this won't be done by resin-multibuild
await Bluebird.map(localImages, async localImage => {
if (localImage.external) {
// We can be sure that localImage.name is set here, because of the failure code above
const image = docker.getImage(localImage.name!);
await image.tag({
repo: generateImageName(localImage.serviceName),
force: true,
});
await image.remove({ force: true });
}
});
}
function assignOutputHandlers(buildTasks: BuildTask[], logger: Logger) {
_.each(buildTasks, task => {
if (task.external) {
task.progressHook = progressObj => {
displayBuildLog(
{ serviceName: task.serviceName, message: progressObj.progress },
logger,
);
};
} else {
task.streamHook = stream => {
stream.on('data', (buf: Buffer) => {
const str = buf.toString().trimRight();
if (str !== '') {
displayBuildLog(
{ serviceName: task.serviceName, message: str },
logger,
);
}
});
};
}
});
}
async function getDeviceDockerImages(docker: Docker): Promise<string[]> {
const images = await docker.listImages();
return _.map(images, 'Id');
}
// Mutates buildTasks
async function assignDockerBuildOpts(
docker: Docker,
buildTasks: BuildTask[],
): Promise<void> {
// Get all of the images on the remote docker daemon, so
// that we can use all of them for cache
const images = await getDeviceDockerImages(docker);
logger.logDebug(`Using ${images.length} on-device images for cache...`);
_.each(buildTasks, (task: BuildTask) => {
task.dockerOpts = {
cachefrom: images,
labels: {
'io.resin.local.image': '1',
'io.resin.local.service': task.serviceName,
},
t: generateImageName(task.serviceName),
};
});
}
function generateImageName(serviceName: string): string {
return `local_image_${serviceName}:latest`;
}
function generateTargetState(
currentTargetState: any,
composition: Composition,
): any {
const services: { [serviceId: string]: any } = {};
let idx = 1;
_.each(composition.services, (opts, name) => {
// Get rid of any build specific stuff
opts = _.cloneDeep(opts);
delete opts.build;
delete opts.image;
const defaults = {
environment: {},
labels: {},
};
services[idx] = _.merge(defaults, opts, {
imageId: idx,
serviceName: name,
serviceId: idx,
image: generateImageName(name),
running: true,
});
idx += 1;
});
const targetState = _.cloneDeep(currentTargetState);
delete targetState.local.apps;
targetState.local.apps = {
1: {
name: 'localapp',
commit: 'localcommit',
releaseId: '1',
services,
volumes: composition.volumes || {},
networks: composition.networks || {},
},
};
return targetState;
}
async function inspectBuildResults(images: LocalImage[]): Promise<void> {
const { exitWithExpectedError } = await import('../patterns');
const failures: LocalPushErrors.BuildFailure[] = [];
_.each(images, image => {
if (!image.successful) {
failures.push({
error: image.error!,
serviceName: image.serviceName,
});
}
});
if (failures.length > 0) {
exitWithExpectedError(new LocalPushErrors.BuildError(failures));
}
}

View File

@ -0,0 +1,30 @@
import * as _ from 'lodash';
import { TypedError } from 'typed-error';
export interface BuildFailure {
error: Error;
serviceName: string;
}
export class BuildError extends TypedError {
private failures: BuildFailure[];
public constructor(failures: BuildFailure[]) {
super('Build error');
this.failures = failures;
}
public toString(): string {
let str = 'Some services failed to build:\n';
_.each(this.failures, failure => {
str += `\t${failure.serviceName}: ${failure.error.message}\n`;
});
return str;
}
}
export class DeviceAPIError extends TypedError {}
export class BadRequestDeviceAPIError extends DeviceAPIError {}
export class ServiceUnavailableAPIError extends DeviceAPIError {}

83
lib/utils/device/logs.ts Normal file
View File

@ -0,0 +1,83 @@
import * as Bluebird from 'bluebird';
import chalk from 'chalk';
import ColorHash = require('color-hash');
import * as _ from 'lodash';
import { Readable } from 'stream';
import Logger = require('../logger');
interface Log {
message: string;
timestamp?: number;
serviceName?: string;
// There's also a serviceId and imageId, but they're
// meaningless in local mode
}
interface BuildLog {
serviceName: string;
message: string;
}
/**
* Display logs from a device logging stream. This function will return
* when the log stream ends.
*
* @param logs A stream which produces newline seperated log objects
*/
export function displayDeviceLogs(
logs: Readable,
logger: Logger,
): Bluebird<void> {
return new Bluebird((resolve, reject) => {
logs.on('data', log => {
displayLogLine(log, logger);
});
logs.on('error', reject);
logs.on('end', resolve);
});
}
export function displayBuildLog(log: BuildLog, logger: Logger): void {
const toPrint = `${getServiceColourFn(log.serviceName)(
`[${log.serviceName}]`,
)} ${log.message}`;
logger.logBuild(toPrint);
}
// mutates serviceColours
function displayLogLine(log: string | Buffer, logger: Logger): void {
try {
const obj: Log = JSON.parse(log.toString());
let toPrint: string;
if (obj.timestamp != null) {
toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`;
} else {
toPrint = `[${new Date().toLocaleString()}]`;
}
if (obj.serviceName != null) {
const colourFn = getServiceColourFn(obj.serviceName);
toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`;
}
toPrint += ` ${obj.message}`;
logger.logLogs(toPrint);
} catch (e) {
logger.logDebug(`Dropping device log due to failed parsing: ${e}`);
}
}
const getServiceColourFn = _.memoize(_getServiceColourFn);
const colorHash = new ColorHash();
function _getServiceColourFn(serviceName: string): (msg: string) => string {
const [r, g, b] = colorHash.rgb(serviceName);
return chalk.rgb(r, g, b);
}

View File

@ -1,7 +1,6 @@
# Functions to help actions which rely on using docker
QEMU_VERSION = 'v2.5.50-resin-execve'
QEMU_BIN_NAME = 'qemu-execve'
Promise = require('bluebird')
# Use this function to seed an action's list of capitano options
# with the docker options. Using this interface means that
@ -71,12 +70,6 @@ exports.appendOptions = (opts) ->
description: "Don't use docker layer caching when building"
boolean: true
},
{
signature: 'emulated'
description: 'Run an emulated build using Qemu'
boolean: true
alias: 'e'
},
{
signature: 'squash'
description: 'Squash newly built layers into a single new layer'
@ -84,8 +77,7 @@ exports.appendOptions = (opts) ->
}
]
exports.generateConnectOpts = generateConnectOpts = (opts) ->
Promise = require('bluebird')
generateConnectOpts = (opts) ->
buildDockerodeOpts = require('dockerode-options')
fs = require('mz/fs')
_ = require('lodash')
@ -131,294 +123,57 @@ exports.generateConnectOpts = generateConnectOpts = (opts) ->
return connectOpts
exports.tarDirectory = tarDirectory = (dir) ->
Promise = require('bluebird')
tar = require('tar-stream')
klaw = require('klaw')
path = require('path')
fs = require('mz/fs')
streamToPromise = require('stream-to-promise')
getFiles = ->
streamToPromise(klaw(dir))
.filter((item) -> not item.stats.isDirectory())
.map((item) -> item.path)
pack = tar.pack()
getFiles(dir)
.map (file) ->
relPath = path.relative(path.resolve(dir), file)
Promise.join relPath, fs.stat(file), fs.readFile(file),
(filename, stats, data) ->
pack.entryAsync({ name: toPosixPath(filename), size: stats.size, mode: stats.mode }, data)
.then ->
pack.finalize()
return pack
cacheHighlightStream = ->
colors = require('colors/safe')
es = require('event-stream')
{ EOL } = require('os')
extractArrowMessage = (message) ->
arrowTest = /^\s*-+>\s*(.+)/i
if (match = arrowTest.exec(message))
match[1]
else
undefined
es.mapSync (data) ->
msg = extractArrowMessage(data)
if msg? and msg.toLowerCase() == 'using cache'
data = colors.bgGreen.black(msg)
return data + EOL
parseBuildArgs = (args, onError) ->
parseBuildArgs = (args) ->
_ = require('lodash')
if not _.isArray(args)
args = [ args ]
buildArgs = {}
args.forEach (str) ->
pair = /^([^\s]+?)=(.*)$/.exec(str)
args.forEach (arg) ->
pair = /^([^\s]+?)=(.*)$/.exec(arg)
if pair?
buildArgs[pair[1]] = pair[2]
buildArgs[pair[1]] = pair[2] ? ''
else
onError(str)
throw new Error("Could not parse build argument: '#{arg}'")
return buildArgs
# Pass in the command line parameters and options and also
# a function which will return the information about the bundle
exports.runBuild = (params, options, getBundleInfo, logger) ->
Promise = require('bluebird')
dockerBuild = require('resin-docker-build')
resolver = require('resin-bundle-resolve')
es = require('event-stream')
doodles = require('resin-doodles')
transpose = require('docker-qemu-transpose')
path = require('path')
# The default build context is the current directory
params.source ?= '.'
logs = ''
# Only used in emulated builds
qemuPath = ''
Promise.try ->
return if not (options.emulated and platformNeedsQemu())
hasQemu()
.then (present) ->
if !present
logger.logInfo('Installing qemu for ARM emulation...')
installQemu()
.then ->
# Copy the qemu binary into the build context
copyQemu(params.source)
.then (binPath) ->
qemuPath = path.relative(params.source, binPath)
.then ->
# Tar up the directory, ready for the build stream
tarDirectory(params.source)
.then (tarStream) ->
new Promise (resolve, reject) ->
hooks =
buildSuccess: (image) ->
# Show charlie. In the interest of cloud parity,
# use console.log, not the standard logging streams
doodle = doodles.getDoodle()
console.log()
console.log(doodle)
console.log()
resolve({ image, log: logs + '\n' + doodle + '\n' } )
buildFailure: reject
buildStream: (stream) ->
if options.emulated
logger.logInfo('Running emulated build')
getBundleInfo(options)
.then (info) ->
if !info?
logger.logWarn '''
Warning: No architecture/device type or application information provided.
Dockerfile/project pre-processing will not be performed.
'''
return tarStream
else
[arch, deviceType] = info
# Perform type resolution on the project
bundle = new resolver.Bundle(tarStream, deviceType, arch)
resolver.resolveBundle(bundle, resolver.getDefaultResolvers())
.then (resolved) ->
logger.logInfo("Building #{resolved.projectType} project")
return resolved.tarStream
.then (buildStream) ->
# if we need emulation
if options.emulated and platformNeedsQemu()
return transpose.transposeTarStream buildStream,
hostQemuPath: toPosixPath(qemuPath)
containerQemuPath: "/tmp/#{QEMU_BIN_NAME}"
else
return buildStream
.then (buildStream) ->
# Send the resolved tar stream to the docker daemon
buildStream.pipe(stream)
.catch(reject)
# And print the output
logThroughStream = es.through (data) ->
logs += data.toString()
this.emit('data', data)
if options.emulated and platformNeedsQemu()
buildThroughStream = transpose.getBuildThroughStream
hostQemuPath: toPosixPath(qemuPath)
containerQemuPath: "/tmp/#{QEMU_BIN_NAME}"
newStream = stream.pipe(buildThroughStream)
else
newStream = stream
newStream
.pipe(logThroughStream)
.pipe(cacheHighlightStream())
.pipe(logger.streams.build)
# Create a builder
generateConnectOpts(options)
.tap (connectOpts) ->
ensureDockerSeemsAccessible(connectOpts)
.then (connectOpts) ->
# Allow degugging output, hidden behind an env var
logger.logDebug('Connecting with the following options:')
logger.logDebug(JSON.stringify(connectOpts, null, ' '))
builder = new dockerBuild.Builder(connectOpts)
opts = {}
if options.tag?
opts['t'] = options.tag
if options.nocache?
opts['nocache'] = true
if options.buildArg?
opts['buildargs'] = parseBuildArgs options.buildArg, (arg) ->
logger.logWarn("Could not parse variable: '#{arg}'")
if options.squash?
opts['squash'] = true
builder.createBuildStream(opts, hooks, reject)
# Given an image id or tag, export the image to a tar archive,
# gzip the result, and buffer it to disk.
exports.bufferImage = (docker, imageId, bufferFile) ->
Promise = require('bluebird')
streamUtils = require('./streams')
image = docker.getImage(imageId)
imageMetadata = image.inspectAsync()
Promise.join image.get(), imageMetadata.get('Size'), (imageStream, imageSize) ->
streamUtils.buffer(imageStream, bufferFile)
.tap (bufferedStream) ->
bufferedStream.length = imageSize
exports.generateBuildOpts = (options) ->
opts = {}
if options.tag?
opts.t = options.tag
if options.nocache?
opts.nocache = true
if options.squash?
opts.squash = true
if options.buildArg?
opts.buildargs = parseBuildArgs(options.buildArg)
return opts
exports.getDocker = (options) ->
Docker = require('dockerode')
Promise = require('bluebird')
generateConnectOpts(options)
.tap (connectOpts) ->
ensureDockerSeemsAccessible(connectOpts)
.then (connectOpts) ->
# Use bluebird's promises
connectOpts['Promise'] = Promise
new Docker(connectOpts)
.then(createClient)
.tap(ensureDockerSeemsAccessible)
ensureDockerSeemsAccessible = (options) ->
fs = require('mz/fs')
exports.createClient = createClient = do ->
# docker-toolbelt v3 is not backwards compatible as it removes all *Async
# methods that are in wide use in the CLI. The workaround for now is to
# manually promisify the client and replace all `new Docker()` calls with
# this shared function that returns a promisified client.
#
# **New code must not use the *Async methods.**
#
Docker = require('docker-toolbelt')
Promise.promisifyAll Docker.prototype, {
filter: (name) -> name == 'run'
multiArgs: true
}
Promise.promisifyAll(Docker.prototype)
Promise.promisifyAll(new Docker({}).getImage().constructor.prototype)
Promise.promisifyAll(new Docker({}).getContainer().constructor.prototype)
if options.socketPath?
# If we're trying to use a socket, check it exists and we have access to it
fs.access(options.socketPath, (fs.constants || fs).R_OK | (fs.constants || fs).W_OK)
.return(true)
.catch (err) ->
throw new Error(
"Docker seems to be unavailable (using socket #{options.socketPath}). Is it
installed, and do you have permission to talk to it?"
)
else
# Otherwise, we think we're probably ok
Promise.resolve(true)
return (opts) ->
return new Docker(opts)
hasQemu = ->
fs = require('mz/fs')
getQemuPath()
.then(fs.stat)
.return(true)
.catchReturn(false)
getQemuPath = ->
resin = require('resin-sdk-preconfigured')
path = require('path')
fs = require('mz/fs')
resin.settings.get('binDirectory')
.then (binDir) ->
# The directory might not be created already,
# if not, create it
fs.access(binDir)
.catch code: 'ENOENT', ->
fs.mkdir(binDir)
.then ->
path.join(binDir, QEMU_BIN_NAME)
platformNeedsQemu = ->
os = require('os')
os.platform() == 'linux'
installQemu = ->
request = require('request')
fs = require('fs')
zlib = require('zlib')
getQemuPath()
.then (qemuPath) ->
new Promise (resolve, reject) ->
installStream = fs.createWriteStream(qemuPath)
qemuUrl = "https://github.com/resin-io/qemu/releases/download/#{QEMU_VERSION}/#{QEMU_BIN_NAME}.gz"
request(qemuUrl)
.pipe(zlib.createGunzip())
.pipe(installStream)
.on('error', reject)
.on('finish', resolve)
copyQemu = (context) ->
path = require('path')
fs = require('mz/fs')
# Create a hidden directory in the build context, containing qemu
binDir = path.join(context, '.resin')
binPath = path.join(binDir, QEMU_BIN_NAME)
fs.access(binDir)
.catch code: 'ENOENT', ->
fs.mkdir(binDir)
.then ->
getQemuPath()
.then (qemu) ->
new Promise (resolve, reject) ->
read = fs.createReadStream(qemu)
write = fs.createWriteStream(binPath)
read
.pipe(write)
.on('error', reject)
.on('finish', resolve)
.then ->
fs.chmod(binPath, '755')
.return(binPath)
toPosixPath = (systemPath) ->
path = require('path')
systemPath.replace(new RegExp('\\' + path.sep, 'g'), '/')
ensureDockerSeemsAccessible = (docker) ->
{ exitWithExpectedError } = require('./patterns')
docker.ping().catch ->
exitWithExpectedError('Docker seems to be unavailable. Is it installed and running?')

View File

@ -19,16 +19,12 @@ import Promise = require('bluebird');
import _ = require('lodash');
import chalk from 'chalk';
import rindle = require('rindle');
import imagefs = require('resin-image-fs');
import visuals = require('resin-cli-visuals');
import ResinSdk = require('resin-sdk');
import { execute } from 'president';
import { InitializeEmitter, OperationState } from 'resin-device-init';
const extractStreamAsync = Promise.promisify(rindle.extract);
const waitStreamAsync = Promise.promisify(rindle.wait);
const presidentExecuteAsync = Promise.promisify(execute);
const resin = ResinSdk.fromSharedOptions();
@ -64,32 +60,40 @@ export function stateToString(state: OperationState) {
}
}
export function sudo(command: string[]) {
export function sudo(
command: string[],
{ stderr, msg }: { stderr?: NodeJS.WritableStream; msg?: string } = {},
) {
const { executeWithPrivileges } = require('./sudo');
if (os.platform() !== 'win32') {
console.log('If asked please type your computer password to continue');
console.log(
msg || 'If asked please type your computer password to continue',
);
}
command = _.union(_.take(process.argv, 2), command);
return executeWithPrivileges(command, stderr);
}
return presidentExecuteAsync(command);
export function runCommand(command: string): Promise<void> {
const capitano = require('capitano');
return Promise.fromCallback(resolver => capitano.run(command, resolver));
}
export function getManifest(
image: string,
deviceType: string,
): Promise<ResinSdk.DeviceType> {
const imagefs = require('resin-image-fs');
// Attempt to read manifest from the first
// partition, but fallback to the API if
// we encounter any errors along the way.
return imagefs
.read({
.readFile({
image,
partition: {
primary: 1,
},
partition: 1,
path: '/device-type.json',
})
.then(extractStreamAsync)
.then(JSON.parse)
.catch(() => resin.models.device.getManifestBySlug(deviceType));
}
@ -133,16 +137,28 @@ export function getArchAndDeviceType(
);
}
function getApplication(applicationName: string) {
export function getApplication(applicationName: string) {
// Check for an app of the form `user/application`, and send
// that off to a special handler (before importing any modules)
const match = /(\w+)\/(\w+)/.exec(applicationName);
const match = applicationName.split('/');
if (match) {
return resin.models.application.getAppByOwner(match[2], match[1]);
const extraOptions = {
$expand: {
application_type: {
$select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'],
},
},
};
if (match.length > 1) {
return resin.models.application.getAppByOwner(
match[1],
match[0],
extraOptions,
);
}
return resin.models.application.get(applicationName);
return resin.models.application.get(applicationName, extraOptions);
}
// A function to reliably execute a command

149
lib/utils/ignore.ts Normal file
View File

@ -0,0 +1,149 @@
import * as _ from 'lodash';
import { fs } from 'mz';
import * as path from 'path';
import dockerIgnore = require('@zeit/dockerignore');
import ignore from 'ignore';
export enum IgnoreFileType {
DockerIgnore,
GitIgnore,
}
interface IgnoreEntry {
pattern: string;
// The relative file path from the base path of the build context
filePath: string;
}
export class FileIgnorer {
private dockerIgnoreEntries: IgnoreEntry[];
private gitIgnoreEntries: IgnoreEntry[];
private static ignoreFiles: Array<{
pattern: string;
type: IgnoreFileType;
allowSubdirs: boolean;
}> = [
{
pattern: '.gitignore',
type: IgnoreFileType.GitIgnore,
allowSubdirs: true,
},
{
pattern: '.dockerignore',
type: IgnoreFileType.DockerIgnore,
allowSubdirs: false,
},
];
public constructor(public basePath: string) {
this.dockerIgnoreEntries = [];
this.gitIgnoreEntries = [];
}
/**
* @param {string} relativePath
* The relative pathname from the build context, for example a root level .gitignore should be
* ./.gitignore
* @returns IgnoreFileType
* The type of ignore file, or null
*/
public getIgnoreFileType(relativePath: string): IgnoreFileType | null {
for (const { pattern, type, allowSubdirs } of FileIgnorer.ignoreFiles) {
if (
path.basename(relativePath) === pattern &&
(allowSubdirs || path.dirname(relativePath) === '.')
) {
return type;
}
}
return null;
}
/**
* @param {string} fullPath
* The full path on disk of the ignore file
* @param {IgnoreFileType} type
* @returns Promise
*/
public async addIgnoreFile(
fullPath: string,
type: IgnoreFileType,
): Promise<void> {
const contents = await fs.readFile(fullPath, 'utf8');
contents.split('\n').forEach(line => {
// ignore empty lines and comments
if (/\s*#/.test(line) || _.isEmpty(line)) {
return;
}
this.addEntry(line, fullPath, type);
});
return;
}
// Pass this function as a predicate to a filter function, and it will filter
// any ignored files
public filter = (filename: string): boolean => {
const dockerIgnoreHandle = dockerIgnore();
const gitIgnoreHandle = ignore();
interface IgnoreHandle {
add: (pattern: string) => void;
ignores: (file: string) => boolean;
}
const ignoreTypes: Array<{
handle: IgnoreHandle;
entries: IgnoreEntry[];
}> = [
{ handle: dockerIgnoreHandle, entries: this.dockerIgnoreEntries },
{ handle: gitIgnoreHandle, entries: this.gitIgnoreEntries },
];
const relFile = path.relative(this.basePath, filename);
_.each(ignoreTypes, ({ handle, entries }) => {
_.each(entries, ({ pattern, filePath }) => {
if (FileIgnorer.contains(path.posix.dirname(filePath), filename)) {
handle.add(pattern);
}
});
});
return !_.some(ignoreTypes, ({ handle }) => handle.ignores(relFile));
};
private addEntry(
pattern: string,
filePath: string,
type: IgnoreFileType,
): void {
const entry: IgnoreEntry = { pattern, filePath };
switch (type) {
case IgnoreFileType.DockerIgnore:
this.dockerIgnoreEntries.push(entry);
break;
case IgnoreFileType.GitIgnore:
this.gitIgnoreEntries.push(entry);
break;
}
}
/**
* Given two paths, check whether the first contains the second
* @param path1 The potentially containing path
* @param path2 The potentially contained path
* @return A boolean indicating whether `path1` contains `path2`
*/
private static contains(path1: string, path2: string): boolean {
// First normalise the input, to remove any path weirdness
path1 = path.posix.normalize(path1);
path2 = path.posix.normalize(path2);
// Now test if the start of the relative path contains ../ ,
// which would tell us that path1 is not part of path2
return !/^\.\.\//.test(path.posix.relative(path1, path2));
}
}

View File

@ -1,6 +1,6 @@
import { EOL as eol } from 'os';
import _ = require('lodash');
import chalk from 'chalk';
import _ = require('lodash');
import { EOL as eol } from 'os';
import { StreamLogger } from 'resin-stream-logger';
class Logger {
@ -11,6 +11,7 @@ class Logger {
success: NodeJS.ReadWriteStream;
warn: NodeJS.ReadWriteStream;
error: NodeJS.ReadWriteStream;
logs: NodeJS.ReadWriteStream;
};
public formatMessage: (name: string, message: string) => string;
@ -23,6 +24,7 @@ class Logger {
logger.addPrefix('success', chalk.green('[Success]'));
logger.addPrefix('warn', chalk.yellow('[Warn]'));
logger.addPrefix('error', chalk.red('[Error]'));
logger.addPrefix('logs', chalk.green('[Logs]'));
this.streams = {
build: logger.createLogStream('build'),
@ -31,6 +33,7 @@ class Logger {
success: logger.createLogStream('success'),
warn: logger.createLogStream('warn'),
error: logger.createLogStream('error'),
logs: logger.createLogStream('logs'),
};
_.forEach(this.streams, function(stream, key) {
@ -61,6 +64,14 @@ class Logger {
logError(msg: string) {
return this.streams.error.write(msg + eol);
}
logBuild(msg: string) {
return this.streams.build.write(msg + eol);
}
logLogs(msg: string) {
return this.streams.logs.write(msg + eol);
}
}
export = Logger;

View File

@ -18,13 +18,11 @@ import _ = require('lodash');
import Promise = require('bluebird');
import form = require('resin-cli-form');
import visuals = require('resin-cli-visuals');
import ResinSdk = require('resin-sdk');
import resin = require('resin-sdk-preconfigured');
import chalk from 'chalk';
import validation = require('./validation');
import messages = require('./messages');
const resin = ResinSdk.fromSharedOptions();
export function authenticate(options: {}): Promise<void> {
return form
.run(
@ -132,10 +130,8 @@ export function confirm(
});
}
export function selectApplication(
filter: (app: ResinSdk.Application) => boolean,
) {
resin.models.application
export function selectApplication(filter: (app: resin.Application) => boolean) {
return resin.models.application
.hasAny()
.then(function(hasAnyApplications) {
if (!hasAnyApplications) {
@ -165,7 +161,7 @@ export function selectOrCreateApplication() {
return resin.models.application.getAll().then(applications => {
const appOptions = _.map<
ResinSdk.Application,
resin.Application,
{ name: string; value: string | null }
>(applications, application => ({
name: `${application.app_name} (${application.device_type})`,
@ -227,13 +223,15 @@ export function awaitDevice(uuid: string) {
export function inferOrSelectDevice(preferredUuid: string) {
return resin.models.device
.getAll()
.filter<ResinSdk.Device>(device => device.is_online)
.filter<resin.Device>(device => device.is_online)
.then(onlineDevices => {
if (_.isEmpty(onlineDevices)) {
throw new Error("You don't have any devices online");
}
const defaultUuid = _.map(onlineDevices, 'uuid').includes(preferredUuid)
const defaultUuid = _(onlineDevices)
.map('uuid')
.includes(preferredUuid)
? preferredUuid
: onlineDevices[0].uuid;
@ -249,12 +247,26 @@ export function inferOrSelectDevice(preferredUuid: string) {
});
}
export function selectFromList<T>(
message: string,
choices: Array<T & { name: string }>,
): Promise<T> {
return form.ask({
message,
type: 'list',
choices: _.map(choices, s => ({
name: s.name,
value: s,
})),
});
}
export function printErrorMessage(message: string) {
console.error(chalk.red(message));
console.error(chalk.red(`\n${messages.getHelp}\n`));
}
export function expectedError(message: string | Error) {
export function exitWithExpectedError(message: string | Error) {
if (message instanceof Error) {
({ message } = message);
}

View File

@ -1,36 +0,0 @@
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import nplugm = require('nplugm');
import _ = require('lodash');
import capitano = require('capitano');
import patterns = require('./patterns');
export function register(regex: RegExp): Promise<void> {
return nplugm
.list(regex)
.map(async function(plugin: any) {
const command = await import(plugin);
command.plugin = true;
if (!_.isArray(command)) {
return capitano.command(command);
}
return _.each(command, capitano.command);
})
.catch((error: Error) => {
return patterns.printErrorMessage(error.message);
});
}

323
lib/utils/promote.ts Normal file
View File

@ -0,0 +1,323 @@
import { stripIndent } from 'common-tags';
import { ResinSDK, Application } from 'resin-sdk';
import Logger = require('./logger');
import { runCommand } from './helpers';
import { exec, execBuffered } from './ssh';
const MIN_RESINOS_VERSION = 'v2.14.0';
export async function join(
logger: Logger,
sdk: ResinSDK,
deviceHostnameOrIp?: string,
appName?: string,
): Promise<void> {
logger.logDebug('Checking login...');
const isLoggedIn = await sdk.auth.isLoggedIn();
if (!isLoggedIn) {
logger.logInfo("Looks like you're not logged in yet!");
logger.logInfo("Let's go through a quick wizard to get you started.\n");
await runCommand('login');
}
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
logger.logDebug('Determining device type...');
const deviceType = await getDeviceType(deviceIp);
logger.logDebug(`Device type: ${deviceType}`);
logger.logDebug('Determining application...');
const app = await getOrSelectApplication(sdk, deviceType, appName);
logger.logDebug(`Using application: ${app.app_name} (${app.device_type})`);
if (app.device_type != deviceType) {
logger.logDebug(`Forcing device type to: ${deviceType}`);
app.device_type = deviceType;
}
logger.logDebug('Generating application config...');
const config = await generateApplicationConfig(sdk, app);
logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`);
logger.logDebug('Configuring...');
await configure(deviceIp, config);
logger.logDebug('All done.');
const platformUrl = await sdk.settings.get('resinUrl');
logger.logSuccess(`Device successfully joined ${platformUrl}!`);
}
export async function leave(
logger: Logger,
_sdk: ResinSDK,
deviceHostnameOrIp?: string,
): Promise<void> {
logger.logDebug('Determining device...');
const deviceIp = await getOrSelectLocalDevice(deviceHostnameOrIp);
await assertDeviceIsCompatible(deviceIp);
logger.logDebug(`Using device: ${deviceIp}`);
logger.logDebug('Deconfiguring...');
await deconfigure(deviceIp);
logger.logDebug('All done.');
logger.logSuccess('Device successfully left the platform.');
}
async function execCommand(
deviceIp: string,
cmd: string,
msg: string,
): Promise<void> {
const through = await import('through2');
const visuals = await import('resin-cli-visuals');
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner;
const stream = through(function(data, _enc, cb) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
cb(null, data);
});
spinner.start();
await exec(deviceIp, cmd, stream);
spinner.stop();
}
async function configure(deviceIp: string, config: any): Promise<void> {
// Passing the JSON is slightly tricky due to the many layers of indirection
// so we just base64-encode it here and decode it at the other end, when invoking
// os-config.
const json = JSON.stringify(config);
const b64 = Buffer.from(json).toString('base64');
const str = `"$(base64 -d <<< ${b64})"`;
await execCommand(deviceIp, `os-config join '${str}'`, 'Configuring...');
}
async function deconfigure(deviceIp: string): Promise<void> {
await execCommand(deviceIp, 'os-config leave', 'Configuring...');
}
async function assertDeviceIsCompatible(deviceIp: string): Promise<void> {
const { exitWithExpectedError } = await import('../utils/patterns');
try {
await execBuffered(deviceIp, 'os-config --version');
} catch (err) {
exitWithExpectedError(stripIndent`
Device "${deviceIp}" is incompatible and cannot join or leave an application.
Please select or provision device with resinOS newer than ${MIN_RESINOS_VERSION}.`);
}
}
async function getDeviceType(deviceIp: string): Promise<string> {
const output = await execBuffered(deviceIp, 'cat /etc/os-release');
const match = /^SLUG="([^"]+)"$/m.exec(output);
if (!match) {
throw new Error('Failed to determine device type');
}
return match[1];
}
async function getOrSelectLocalDevice(deviceIp?: string): Promise<string> {
if (deviceIp) {
return deviceIp;
}
const through = await import('through2');
let ip: string | null = null;
const stream = through(function(data, _enc, cb) {
const match = /^==> Selected device: (.*)$/m.exec(data.toString());
if (match) {
ip = match[1];
cb();
} else {
cb(null, data);
}
});
stream.pipe(process.stderr);
const { sudo } = await import('../utils/helpers');
const command = process.argv.slice(0, 2).concat(['internal', 'scandevices']);
await sudo(command, {
stderr: stream,
msg:
'Scanning for local devices. If asked, please type your computer password.',
});
if (!ip) {
throw new Error('No device selected');
}
return ip;
}
async function getOrSelectApplication(
sdk: ResinSDK,
deviceType: string,
appName?: string,
): Promise<Application> {
const _ = await import('lodash');
const form = await import('resin-cli-form');
const { selectFromList } = await import('../utils/patterns');
const allDeviceTypes = await sdk.models.config.getDeviceTypes();
const deviceTypeManifest = _.find(allDeviceTypes, { slug: deviceType });
if (!deviceTypeManifest) {
throw new Error(`"${deviceType}" is not a valid device type`);
}
const compatibleDeviceTypes = _(allDeviceTypes)
.filter({ arch: deviceTypeManifest.arch })
.map(type => type.slug)
.value();
const options: any = {
$expand: { user: { $select: ['username'] } },
$filter: { device_type: { $in: compatibleDeviceTypes } },
};
if (!appName) {
// No application specified, show a list to select one.
const applications = await sdk.models.application.getAll(options);
if (applications.length === 0) {
const shouldCreateApp = await form.ask({
message:
'You have no applications this device can join.\n' +
'Would you like to create one now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType);
}
process.exit(1);
}
return selectFromList(
'Select application',
_.map(applications, app => _.merge({ name: app.app_name }, app)),
);
}
// We're given an application; resolve it if it's ambiguous and also validate
// it's of appropriate device type.
options.$filter = { app_name: appName };
// Check for an app of the form `user/application` and update the API query.
const match = appName.split('/');
if (match.length > 1) {
// These will match at most one app, so we'll return early.
options.$expand.user.$filter = { username: match[0] };
options.$filter.app_name = match[1];
}
// Fetch all applications with the given name that are accessible to the user
const applications = await sdk.pine.get<Application>({
resource: 'application',
options,
});
if (applications.length === 0) {
const shouldCreateApp = await form.ask({
message:
`No application found with name "${appName}".\n` +
'Would you like to create it now?',
type: 'confirm',
default: true,
});
if (shouldCreateApp) {
return createApplication(sdk, deviceType, options.$filter.app_name);
}
process.exit(1);
}
// We've found at least one app with the given name.
// Filter out apps for non-matching device types and see what we're left with.
const validApplications = applications.filter(app =>
_.includes(compatibleDeviceTypes, app.device_type),
);
if (validApplications.length === 1) {
return validApplications[0];
}
// If we got more than one application with the same name it means that the
// user has access to a collab app with the same name as a personal app. We
// present a list to the user which shows the fully qualified application
// name (user/appname) and allows them to select
return selectFromList(
'Found multiple applications with that name; please select the one to use',
_.map(validApplications, app => {
const owner = _.get(app, 'user[0].username');
return _.merge({ name: `${owner}/${app.app_name}` }, app);
}),
);
}
async function createApplication(
sdk: ResinSDK,
deviceType: string,
name?: string,
): Promise<Application> {
const form = await import('resin-cli-form');
const validation = await import('./validation');
const patterns = await import('./patterns');
const user = await sdk.auth.getUserId();
const queryOptions = {
$filter: { user },
};
const appName = await new Promise<string>(async (resolve, reject) => {
while (true) {
try {
const appName = await form.ask({
message: 'Enter a name for your new application:',
type: 'input',
default: name,
validate: validation.validateApplicationName,
});
try {
await sdk.models.application.get(appName, queryOptions);
patterns.printErrorMessage(
'You already have an application with that name; please choose another.',
);
continue;
} catch (err) {
return resolve(appName);
}
} catch (err) {
return reject(err);
}
}
});
return sdk.models.application.create({
name: appName,
deviceType,
});
}
async function generateApplicationConfig(sdk: ResinSDK, app: Application) {
const form = await import('resin-cli-form');
const { generateApplicationConfig: configGen } = await import('./config');
const manifest = await sdk.models.device.getManifestBySlug(app.device_type);
const opts =
manifest.options && manifest.options.filter(opt => opt.name !== 'network');
const values = await form.run(opts);
const config = await configGen(app, values);
if (config.connectivity === 'connman') {
delete config.connectivity;
delete config.files;
}
return config;
}

86
lib/utils/qemu.coffee Normal file
View File

@ -0,0 +1,86 @@
Promise = require('bluebird')
exports.QEMU_VERSION = QEMU_VERSION = 'v2.5.50-resin-execve'
exports.QEMU_BIN_NAME = QEMU_BIN_NAME = 'qemu-execve'
exports.installQemuIfNeeded = Promise.method (emulated, logger) ->
return false if not (emulated and platformNeedsQemu())
hasQemu()
.then (present) ->
if !present
logger.logInfo('Installing qemu for ARM emulation...')
installQemu()
.return(true)
exports.qemuPathInContext = (context) ->
path = require('path')
binDir = path.join(context, '.resin')
binPath = path.join(binDir, QEMU_BIN_NAME)
path.relative(context, binPath)
exports.copyQemu = (context) ->
path = require('path')
fs = require('mz/fs')
# Create a hidden directory in the build context, containing qemu
binDir = path.join(context, '.resin')
binPath = path.join(binDir, QEMU_BIN_NAME)
Promise.resolve(fs.mkdir(binDir))
.catch(code: 'EEXIST', ->)
.then ->
getQemuPath()
.then (qemu) ->
new Promise (resolve, reject) ->
read = fs.createReadStream(qemu)
write = fs.createWriteStream(binPath)
read
.pipe(write)
.on('error', reject)
.on('finish', resolve)
.then ->
fs.chmod(binPath, '755')
.then ->
path.relative(context, binPath)
hasQemu = ->
fs = require('mz/fs')
getQemuPath()
.then(fs.stat)
.return(true)
.catchReturn(false)
getQemuPath = ->
resin = require('resin-sdk').fromSharedOptions()
path = require('path')
fs = require('mz/fs')
resin.settings.get('binDirectory')
.then (binDir) ->
Promise.resolve(fs.mkdir(binDir))
.catch(code: 'EEXIST', ->)
.then ->
path.join(binDir, QEMU_BIN_NAME)
platformNeedsQemu = ->
os = require('os')
os.platform() == 'linux'
installQemu = ->
request = require('request')
fs = require('fs')
zlib = require('zlib')
getQemuPath()
.then (qemuPath) ->
new Promise (resolve, reject) ->
installStream = fs.createWriteStream(qemuPath)
qemuUrl = "https://github.com/resin-io/qemu/releases/download/#{QEMU_VERSION}/#{QEMU_BIN_NAME}.gz"
request(qemuUrl)
.on('error', reject)
.pipe(zlib.createGunzip())
.on('error', reject)
.pipe(installStream)
.on('error', reject)
.on('finish', resolve)

255
lib/utils/remote-build.ts Normal file
View File

@ -0,0 +1,255 @@
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as JSONStream from 'JSONStream';
import * as request from 'request';
import { ResinSDK } from 'resin-sdk';
import * as Stream from 'stream';
import { TypedError } from 'typed-error';
import { tarDirectory } from './compose';
const DEBUG_MODE = !!process.env.DEBUG;
const CURSOR_METADATA_REGEX = /([a-z]+)([0-9]+)?/;
const TRIM_REGEX = /\n+$/;
export interface BuildOpts {
emulated: boolean;
nocache: boolean;
}
export interface RemoteBuild {
app: string;
owner: string;
source: string;
auth: string;
baseUrl: string;
opts: BuildOpts;
sdk: ResinSDK;
// For internal use
releaseId?: number;
hadError?: boolean;
}
interface BuilderMessage {
message: string;
type?: string;
replace?: boolean;
isError?: boolean;
// These will be set when the type === 'metadata'
resource?: string;
value?: string;
}
export class RemoteBuildFailedError extends TypedError {
public constructor(message = 'Remote build failed') {
super(message);
}
}
async function getBuilderEndpoint(
baseUrl: string,
owner: string,
app: string,
opts: BuildOpts,
): Promise<string> {
const querystring = await import('querystring');
const args = querystring.stringify({
owner,
app,
emulated: opts.emulated,
nocache: opts.nocache,
});
return `https://builder.${baseUrl}/v3/build?${args}`;
}
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
const Bluebird = await import('bluebird');
const stream = await getRequestStream(build);
// Special windows handling (win64 also reports win32)
if (process.platform === 'win32') {
const readline = (await import('readline')).createInterface({
input: process.stdin,
output: process.stdout,
});
readline.on('SIGINT', () => process.emit('SIGINT'));
}
return new Bluebird((resolve, reject) => {
// Setup interrupt handlers so we can cancel the build if the user presses
// ctrl+c
// This is necessary because the `exit-hook` module is used by several
// dependencies, and will exit without calling the following handler.
// Once https://github.com/resin-io/resin-cli/issues/867 has been solved,
// we are free to (and definitely should) remove the below line
process.removeAllListeners('SIGINT');
process.on('SIGINT', () => {
process.stderr.write('Received SIGINT, cleaning up. Please wait.\n');
cancelBuildIfNecessary(build).then(() => {
stream.end();
process.exit(130);
});
});
stream.on('data', getBuilderMessageHandler(build));
stream.on('end', resolve);
stream.on('error', reject);
}).then(() => {
if (build.hadError) {
throw new RemoteBuildFailedError();
}
});
}
async function handleBuilderMetadata(obj: BuilderMessage, build: RemoteBuild) {
const { stripIndent } = await import('common-tags');
switch (obj.resource) {
case 'cursor':
const readline = await import('readline');
if (obj.value == null) {
return;
}
const match = obj.value.match(CURSOR_METADATA_REGEX);
if (!match) {
// FIXME: Make this error nicer.
console.log(
stripIndent`
Warning: ignoring unknown builder command. You may experience
odd build output. Maybe you need to update resin-cli?`,
);
return;
}
const value = match[1];
const amount = match[2] || 1;
switch (value) {
case 'erase':
readline.clearLine(process.stdout, 0);
process.stdout.write('\r');
break;
case 'up':
readline.moveCursor(process.stdout, 0, -amount);
break;
case 'down':
readline.moveCursor(process.stdout, 0, amount);
break;
}
break;
case 'buildLogId':
// The name of this resource is slightly dated, but this is the release
// id from the API. We need to save this so that if the user ctrl+c's the
// build we can cancel it on the API.
build.releaseId = parseInt(obj.value!, 10);
break;
}
}
function getBuilderMessageHandler(
build: RemoteBuild,
): (obj: BuilderMessage) => Promise<void> {
return async (obj: BuilderMessage) => {
if (DEBUG_MODE) {
console.log(`[debug] handling message: ${JSON.stringify(obj)}`);
}
if (obj.type != null && obj.type === 'metadata') {
return handleBuilderMetadata(obj, build);
}
if (obj.message) {
const readline = await import('readline');
readline.clearLine(process.stdout, 0);
const message = obj.message.replace(TRIM_REGEX, '');
if (obj.replace) {
process.stdout.write(`\r${message}`);
} else {
process.stdout.write(`\r${message}\n`);
}
}
if (obj.isError) {
build.hadError = true;
}
};
}
async function cancelBuildIfNecessary(build: RemoteBuild): Promise<void> {
if (build.releaseId != null) {
await build.sdk.pine.patch({
resource: 'release',
id: build.releaseId,
body: {
status: 'cancelled',
end_timestamp: Date.now(),
},
});
}
}
async function getRequestStream(build: RemoteBuild): Promise<Stream.Duplex> {
const path = await import('path');
const visuals = await import('resin-cli-visuals');
const zlib = await import('zlib');
const tarSpinner = new visuals.Spinner('Packaging the project source...');
tarSpinner.start();
// Tar the directory so that we can send it to the builder
const tarStream = await tarDirectory(path.resolve(build.source));
tarSpinner.stop();
if (DEBUG_MODE) {
console.log('[debug] Opening builder connection');
}
const post = request.post({
url: await getBuilderEndpoint(
build.baseUrl,
build.owner,
build.app,
build.opts,
),
auth: {
bearer: build.auth,
},
headers: {
'Content-Encoding': 'gzip',
},
body: tarStream.pipe(
zlib.createGzip({
level: 6,
}),
),
});
const uploadSpinner = new visuals.Spinner(
'Uploading source package to resin cloud',
);
uploadSpinner.start();
const parseStream = post.pipe(JSONStream.parse('*'));
parseStream.on('data', () => uploadSpinner.stop());
return parseStream as Stream.Duplex;
}

65
lib/utils/ssh.ts Normal file
View File

@ -0,0 +1,65 @@
import { spawn } from 'child_process';
import * as Bluebird from 'bluebird';
import { TypedError } from 'typed-error';
import { getSubShellCommand } from './helpers';
export class ExecError extends TypedError {
public cmd: string;
public exitCode: number;
constructor(cmd: string, exitCode: number) {
super(`Command '${cmd}' failed with error: ${exitCode}`);
this.cmd = cmd;
this.exitCode = exitCode;
}
}
export async function exec(
deviceIp: string,
cmd: string,
stdout?: NodeJS.WritableStream,
): Promise<void> {
const command = `ssh \
-t \
-p 22222 \
-o LogLevel=ERROR \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
root@${deviceIp} \
${cmd}`;
const stdio = ['ignore', stdout ? 'pipe' : 'inherit', 'ignore'];
const { program, args } = getSubShellCommand(command);
const exitCode = await new Bluebird<number>((resolve, reject) => {
const ps = spawn(program, args, { stdio })
.on('error', reject)
.on('close', resolve);
if (stdout) {
ps.stdout.pipe(stdout);
}
});
if (exitCode != 0) {
throw new ExecError(cmd, exitCode);
}
}
export async function execBuffered(
deviceIp: string,
cmd: string,
enc?: string,
): Promise<string> {
const through = await import('through2');
const buffer: string[] = [];
await exec(
deviceIp,
cmd,
through(function(data, _enc, cb) {
buffer.push(data.toString(enc));
cb();
}),
);
return buffer.join('');
}

26
lib/utils/sudo.ts Normal file
View File

@ -0,0 +1,26 @@
import { spawn } from 'child_process';
import * as Bluebird from 'bluebird';
import * as rindle from 'rindle';
export async function executeWithPrivileges(
command: string[],
stderr?: NodeJS.WritableStream,
): Promise<void> {
const opts = {
stdio: ['inherit', 'inherit', stderr ? 'pipe' : 'inherit'],
env: process.env,
};
const args = process.argv
.slice(0, 2)
.concat(['internal', 'sudo', command.join(' ')]);
const ps = spawn(args[0], args.slice(1), opts);
if (stderr) {
ps.stderr.pipe(stderr);
}
return Bluebird.fromCallback(resolver => rindle.wait(ps, resolver));
}

66
lib/utils/tty.coffee Normal file
View File

@ -0,0 +1,66 @@
windowSize = {}
updateWindowSize = ->
size = require('window-size')?.get()
windowSize.width = size?.width
windowSize.height = size?.height
process.stdout.on('resize', updateWindowSize)
module.exports = (stream = process.stdout) ->
# make sure we get initial metrics
updateWindowSize()
currentWindowSize = ->
# always return a copy.
# width/height can be undefined if no TTY.
width: windowSize.width
height: windowSize.height
hideCursor = ->
stream.write('\u001B[?25l')
showCursor = ->
stream.write('\u001B[?25h')
cursorUp = (rows = 0) ->
stream.write("\u001B[#{rows}A")
cursorDown = (rows = 0) ->
stream.write("\u001B[#{rows}B")
cursorHidden = ->
Promise = require('bluebird')
Promise.try(hideCursor).disposer(showCursor)
write = (str) ->
stream.write(str)
writeLine = (str) ->
stream.write("#{str}\n")
clearLine = ->
stream.write('\u001B[2K\r')
replaceLine = (str) ->
clearLine()
write(str)
deleteToEnd = ->
stream.write('\u001b[0J')
return {
stream
currentWindowSize
hideCursor
showCursor
cursorHidden
cursorUp
cursorDown
write
writeLine
clearLine
replaceLine
deleteToEnd
}

View File

@ -1,6 +1,6 @@
{
"name": "resin-cli",
"version": "6.13.1",
"version": "8.0.0",
"description": "The official resin.io CLI tool",
"main": "./build/actions/index.js",
"homepage": "https://github.com/resin-io/resin-cli",
@ -21,7 +21,9 @@
"pkg": {
"scripts": [
"node_modules/resin-sync/build/capitano/*.js",
"node_modules/resin-sync/build/sync/*.js"
"node_modules/resin-sync/build/sync/*.js",
"node_modules/resin-compose-parse/src/schemas/*.json",
"node_modules/raven/lib/instrumentation/*.js"
],
"assets": [
"build/auth/pages/*.ejs",
@ -31,15 +33,17 @@
"scripts": {
"prebuild": "rimraf build/ build-bin/ build-zip/",
"build": "npm run build:src && npm run build:bin",
"build:src": "npm run prettify && npm run lint && gulp build && tsc && npm run build:doc",
"build:src": "npm run prettify && npm run lint && npm run build:fast && npm run build:doc",
"build:fast": "gulp build && tsc",
"build:doc": "mkdirp doc/ && ts-node automation/capitanodoc/index.ts > doc/cli.markdown",
"build:bin": "ts-node --type-check -P automation automation/build-bin.ts",
"release": "npm run build && ts-node --type-check -P automation automation/deploy-bin.ts",
"pretest": "npm run build",
"test": "gulp test",
"test:fast": "npm run build:fast && gulp test",
"ci": "npm run test && catch-uncommitted",
"watch": "gulp watch",
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\"",
"prettify": "prettier --write \"{lib,tests,automation,typings}/**/*.ts\" --config ./node_modules/resin-lint/config/.prettierrc",
"lint": "resin-lint lib/ tests/ && resin-lint --typescript automation/ lib/ typings/ tests/",
"prepublish": "require-npm4-to-publish",
"prepublishOnly": "npm run build"
@ -54,13 +58,17 @@
"node": ">=6.0"
},
"devDependencies": {
"@types/archiver": "^2.0.1",
"@types/bluebird": "^3.5.19",
"@types/fs-extra": "^5.0.0",
"@types/is-root": "^1.0.0",
"@types/mkdirp": "^0.5.2",
"@types/archiver": "2.1.2",
"@types/bluebird": "3.5.19",
"@types/common-tags": "1.4.0",
"@types/es6-promise": "0.0.32",
"@types/fs-extra": "5.0.4",
"@types/is-root": "1.0.0",
"@types/lodash": "4.14.103",
"@types/mkdirp": "0.5.2",
"@types/node": "10.5.2",
"@types/prettyjson": "0.0.28",
"@types/raven": "^2.1.2",
"@types/raven": "2.5.1",
"catch-uncommitted": "^1.0.0",
"ent": "^2.2.0",
"filehound": "^1.16.2",
@ -72,15 +80,20 @@
"gulp-shell": "^0.5.2",
"mochainon": "^2.0.0",
"pkg": "^4.3.0-beta.1",
"prettier": "^1.9.2",
"prettier": "^1.14.2",
"publish-release": "^1.3.3",
"require-npm4-to-publish": "^1.0.0",
"resin-lint": "^1.5.0",
"resin-lint": "^2.0.0",
"rewire": "^3.0.2",
"ts-node": "^4.0.1",
"typescript": "2.4.0"
"typescript": "2.8.1"
},
"dependencies": {
"@resin.io/valid-email": "^0.1.0",
"@types/stream-to-promise": "2.2.0",
"@types/through2": "^2.0.33",
"@zeit/dockerignore": "0.0.1",
"JSONStream": "^1.0.3",
"ansi-escapes": "^2.0.0",
"any-promise": "^1.3.0",
"archiver": "^2.1.0",
@ -89,64 +102,80 @@
"body-parser": "^1.14.1",
"capitano": "^1.7.0",
"chalk": "^2.3.0",
"cli-truncate": "^1.1.0",
"coffeescript": "^1.12.6",
"color-hash": "^1.0.3",
"columnify": "^1.5.2",
"common-tags": "^1.7.2",
"denymount": "^2.2.0",
"docker-qemu-transpose": "^0.2.2",
"docker-toolbelt": "^1.3.3",
"dockerode": "^2.5.0",
"docker-progress": "^3.0.1",
"docker-qemu-transpose": "^0.5.1",
"docker-toolbelt": "^3.1.0",
"dockerode": "^2.5.5",
"dockerode-options": "^0.2.1",
"drivelist": "^5.0.22",
"ejs": "^2.5.7",
"etcher-image-write": "^9.0.3",
"express": "^4.13.3",
"ext2fs": "1.0.7",
"global-tunnel-ng": "^2.1.1",
"hasbin": "^1.2.3",
"humanize": "0.0.9",
"ignore": "^5.0.2",
"inquirer": "^3.1.1",
"is-root": "^1.0.0",
"js-yaml": "^3.7.0",
"klaw": "^1.3.1",
"js-yaml": "^3.10.0",
"klaw": "^3.0.0",
"lodash": "^4.17.4",
"minimatch": "^3.0.4",
"mixpanel": "^0.4.0",
"mkdirp": "^0.5.1",
"moment": "^2.12.0",
"moment": "^2.20.1",
"moment-duration-format": "^2.2.1",
"mz": "^2.6.0",
"node-cleanup": "^2.1.2",
"nplugm": "^3.0.0",
"opn": "^5.1.0",
"president": "^2.0.1",
"prettyjson": "^1.1.3",
"progress-stream": "^2.0.0",
"raven": "^1.2.0",
"reconfix": "^0.0.3",
"raven": "^2.5.0",
"reconfix": "^0.1.0",
"request": "^2.81.0",
"resin-bundle-resolve": "^0.0.2",
"resin-cli-errors": "^1.2.0",
"resin-cli-form": "^1.4.1",
"resin-bundle-resolve": "^0.6.0",
"resin-cli-form": "^2.0.0",
"resin-cli-visuals": "^1.4.0",
"resin-compose-parse": "^2.0.0",
"resin-config-json": "^1.0.0",
"resin-device-config": "^4.0.0",
"resin-device-init": "^4.0.0",
"resin-docker-build": "^0.4.0",
"resin-doodles": "0.0.1",
"resin-image-fs": "^2.3.0",
"resin-image-fs": "^5.0.2",
"resin-image-manager": "^5.0.0",
"resin-preload": "^5.0.0",
"resin-sdk": "^7.0.0",
"resin-multibuild": "^0.9.0",
"resin-preload": "^7.0.0",
"resin-release": "^1.2.0",
"resin-sdk": "10.0.0-beta2",
"resin-sdk-preconfigured": "^6.9.0",
"resin-semver": "^1.3.0",
"resin-settings-client": "^3.6.1",
"resin-stream-logger": "^0.1.0",
"resin-sync": "^9.2.3",
"resin-sync": "^9.3.3",
"rimraf": "^2.4.3",
"rindle": "^1.0.0",
"semver": "^5.3.0",
"stream-to-promise": "^2.2.0",
"split": "^1.0.1",
"string-width": "^2.1.1",
"strip-ansi-stream": "^1.0.0",
"tar-stream": "^1.5.5",
"through2": "^2.0.3",
"tmp": "0.0.31",
"typed-error": "^3.0.0",
"umount": "^1.1.6",
"unzip2": "^0.2.5",
"update-notifier": "^2.2.0"
"update-notifier": "^2.2.0",
"window-size": "^1.1.0"
},
"optionalDependencies": {
"removedrive": "^1.0.0"
"removedrive": "^1.0.0",
"windosu": "^0.2.0"
}
}
}

73
resin-completion.bash Normal file
View File

@ -0,0 +1,73 @@
#!/bin/bash
_resin_complete()
{
local cur prev
# Valid top-level completions
commands="app apps build config deploy device devices env envs help key \
keys local login logout logs note os preload quickstart settings \
signup ssh sync util version whoami"
# Sub-completions
app_cmds="create restart rm"
config_cmds="generate inject read reconfigure write"
device_cmds="identify init move public-url reboot register rename rm \
shutdown"
device_public_url_cmds="disable enable status"
env_cmds="add rename rm"
key_cmds="add rm"
local_cmds="configure flash logs push scan ssh stop"
os_cmds="build-config configure download initialize versions"
util_cmds="available-drives"
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
if [ $COMP_CWORD -eq 1 ]
then
COMPREPLY=( $(compgen -W "${commands}" -- $cur) )
elif [ $COMP_CWORD -eq 2 ]
then
case "$prev" in
"app")
COMPREPLY=( $(compgen -W "$app_cmds" -- $cur) )
;;
"config")
COMPREPLY=( $(compgen -W "$config_cmds" -- $cur) )
;;
"device")
COMPREPLY=( $(compgen -W "$device_cmds" -- $cur) )
;;
"env")
COMPREPLY=( $(compgen -W "$env_cmds" -- $cur) )
;;
"key")
COMPREPLY=( $(compgen -W "$key_cmds" -- $cur) )
;;
"local")
COMPREPLY=( $(compgen -W "$local_cmds" -- $cur) )
;;
"os")
COMPREPLY=( $(compgen -W "$os_cmds" -- $cur) )
;;
"util")
COMPREPLY=( $(compgen -W "$util_cmds" -- $cur) )
;;
"*")
;;
esac
elif [ $COMP_CWORD -eq 3 ]
then
case "$prev" in
"public-url")
COMPREPLY=( $(compgen -W "$device_public_url_cmds" -- $cur) )
;;
"*")
;;
esac
fi
}
complete -F _resin_complete resin

View File

@ -51,11 +51,11 @@ describe 'Server:', ->
describe 'given the token authenticates with the server', ->
beforeEach ->
@utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid')
@utilsIsTokenValidStub.returns(Promise.resolve(true))
@loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid')
@loginIfTokenValidStub.returns(Promise.resolve(true))
afterEach ->
@utilsIsTokenValidStub.restore()
@loginIfTokenValidStub.restore()
it 'should eventually be the token', (done) ->
promise = server.awaitForToken(options)
@ -74,11 +74,11 @@ describe 'Server:', ->
describe 'given the token does not authenticate with the server', ->
beforeEach ->
@utilsIsTokenValidStub = m.sinon.stub(utils, 'isTokenValid')
@utilsIsTokenValidStub.returns(Promise.resolve(false))
@loginIfTokenValidStub = m.sinon.stub(utils, 'loginIfTokenValid')
@loginIfTokenValidStub.returns(Promise.resolve(false))
afterEach ->
@utilsIsTokenValidStub.restore()
@loginIfTokenValidStub.restore()
it 'should be rejected', (done) ->
promise = server.awaitForToken(options)

View File

@ -1,64 +1,64 @@
m = require('mochainon')
url = require('url')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
utils = require('../../build/auth/utils')
tokens = require('./tokens.json')
rewire = require('rewire')
utils = rewire('../../build/auth/utils')
resin = utils.__get__('resin')
describe 'Utils:', ->
describe '.getDashboardLoginURL()', ->
it 'should eventually be a valid url', (done) ->
it 'should eventually be a valid url', ->
utils.getDashboardLoginURL('https://127.0.0.1:3000/callback').then (loginUrl) ->
m.chai.expect ->
url.parse(loginUrl)
.to.not.throw(Error)
.nodeify(done)
it 'should eventually contain an https protocol', (done) ->
it 'should eventually contain an https protocol', ->
Promise.props
dashboardUrl: resin.settings.get('dashboardUrl')
loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback')
.then ({ dashboardUrl, loginUrl }) ->
protocol = url.parse(loginUrl).protocol
m.chai.expect(protocol).to.equal(url.parse(dashboardUrl).protocol)
.nodeify(done)
it 'should correctly escape a callback url without a path', (done) ->
it 'should correctly escape a callback url without a path', ->
Promise.props
dashboardUrl: resin.settings.get('dashboardUrl')
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000')
.then ({ dashboardUrl, loginUrl }) ->
expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000"
m.chai.expect(loginUrl).to.equal(expectedUrl)
.nodeify(done)
it 'should correctly escape a callback url with a path', (done) ->
it 'should correctly escape a callback url with a path', ->
Promise.props
dashboardUrl: resin.settings.get('dashboardUrl')
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback')
.then ({ dashboardUrl, loginUrl }) ->
expectedUrl = "#{dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback"
m.chai.expect(loginUrl).to.equal(expectedUrl)
.nodeify(done)
describe '.isTokenValid()', ->
describe '.loginIfTokenValid()', ->
it 'should eventually be false if token is undefined', ->
promise = utils.isTokenValid(undefined)
promise = utils.loginIfTokenValid(undefined)
m.chai.expect(promise).to.eventually.be.false
it 'should eventually be false if token is null', ->
promise = utils.isTokenValid(null)
promise = utils.loginIfTokenValid(null)
m.chai.expect(promise).to.eventually.be.false
it 'should eventually be false if token is an empty string', ->
promise = utils.isTokenValid('')
promise = utils.loginIfTokenValid('')
m.chai.expect(promise).to.eventually.be.false
it 'should eventually be false if token is a string containing only spaces', ->
promise = utils.isTokenValid(' ')
promise = utils.loginIfTokenValid(' ')
m.chai.expect(promise).to.eventually.be.false
describe 'given the token does not authenticate with the server', ->
@ -71,31 +71,31 @@ describe 'Utils:', ->
@resinAuthIsLoggedInStub.restore()
it 'should eventually be false', ->
promise = utils.isTokenValid(tokens.johndoe.token)
promise = utils.loginIfTokenValid(tokens.johndoe.token)
m.chai.expect(promise).to.eventually.be.false
describe 'given there was a token already', ->
beforeEach (done) ->
resin.auth.loginWithToken(tokens.janedoe.token).nodeify(done)
beforeEach ->
resin.auth.loginWithToken(tokens.janedoe.token)
it 'should preserve the old token', (done) ->
it 'should preserve the old token', ->
resin.auth.getToken().then (originalToken) ->
m.chai.expect(originalToken).to.equal(tokens.janedoe.token)
return utils.isTokenValid(tokens.johndoe.token)
return utils.loginIfTokenValid(tokens.johndoe.token)
.then(resin.auth.getToken).then (currentToken) ->
m.chai.expect(currentToken).to.equal(tokens.janedoe.token)
.nodeify(done)
describe 'given there was no token', ->
beforeEach (done) ->
resin.auth.logout().nodeify(done)
beforeEach ->
resin.auth.logout()
it 'should stay without a token', (done) ->
utils.isTokenValid(tokens.johndoe.token).then ->
m.chai.expect(resin.token.get()).to.eventually.not.exist
.nodeify(done)
it 'should stay without a token', ->
utils.loginIfTokenValid(tokens.johndoe.token).then ->
resin.auth.isLoggedIn()
.then (isLoggedIn) ->
m.chai.expect(isLoggedIn).to.equal(false)
describe 'given the token does authenticate with the server', ->
@ -107,5 +107,5 @@ describe 'Utils:', ->
@resinAuthIsLoggedInStub.restore()
it 'should eventually be true', ->
promise = utils.isTokenValid(tokens.johndoe.token)
promise = utils.loginIfTokenValid(tokens.johndoe.token)
m.chai.expect(promise).to.eventually.be.true

View File

@ -0,0 +1,85 @@
require 'mocha'
chai = require 'chai'
_ = require 'lodash'
path = require('path')
expect = chai.expect
{ FileIgnorer, IgnoreFileType } = require '../../build/utils/ignore'
describe 'File ignorer', ->
it 'should detect ignore files', ->
f = new FileIgnorer('.' + path.sep)
expect(f.getIgnoreFileType('.gitignore')).to.equal(IgnoreFileType.GitIgnore)
expect(f.getIgnoreFileType('.dockerignore')).to.equal(IgnoreFileType.DockerIgnore)
expect(f.getIgnoreFileType('./.gitignore')).to.equal(IgnoreFileType.GitIgnore)
expect(f.getIgnoreFileType('./.dockerignore')).to.equal(IgnoreFileType.DockerIgnore)
# gitignore files can appear in subdirectories, but dockerignore files cannot
expect(f.getIgnoreFileType('./subdir/.gitignore')).to.equal(IgnoreFileType.GitIgnore)
expect(f.getIgnoreFileType('./subdir/.dockerignore')).to.equal(null)
expect(f.getIgnoreFileType('./subdir/subdir2/.gitignore')).to.equal(IgnoreFileType.GitIgnore)
expect(f.getIgnoreFileType('file')).to.equal(null)
expect(f.getIgnoreFileType('./file')).to.equal(null)
it 'should filter files from the root directory', ->
ignore = new FileIgnorer('.' + path.sep)
ignore.gitIgnoreEntries = [
{ pattern: '*.ignore', filePath: '.gitignore' }
]
ignore.dockerIgnoreEntries = [
{ pattern: '*.ignore2', filePath: '.dockerignore' }
]
files = [
'a'
'a/b'
'a/b/c'
'file.ignore'
'file2.ignore'
'file.ignore2'
'file2.ignore'
]
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'a'
'a/b'
'a/b/c'
])
it 'should filter files from subdirectories', ->
ignore = new FileIgnorer('.' + path.sep)
ignore.gitIgnoreEntries = [
{ pattern: '*.ignore', filePath: 'lib/.gitignore' }
]
files = [
'test.ignore'
'root.ignore'
'lib/normal-file'
'lib/should.ignore'
'lib/thistoo.ignore'
]
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'test.ignore'
'root.ignore'
'lib/normal-file'
])
ignore.gitIgnoreEntries = [
{ pattern: '*.ignore', filePath: './lib/.gitignore' }
]
files = [
'test.ignore'
'root.ignore'
'lib/normal-file'
'lib/should.ignore'
'lib/thistoo.ignore'
]
expect(_.filter(files, ignore.filter.bind(ignore))).to.deep.equal([
'test.ignore'
'root.ignore'
'lib/normal-file'
])

View File

@ -1,14 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es6",
"outDir": "build",
"strict": true,
"strictPropertyInitialization": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"skipLibCheck": true,
"lib": [
// es5 defaults:
"dom",

53
typings/JSONStream.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
// These are the DefinitelyTyped typings for JSONStream, but because of this
// mismatch in case of jsonstream and JSONStream, it is necessary to include
// them this way, with an upper case module declaration. They have also
// been slightly edited to remove the extra `declare` keyworks (which are
// not necessary or accepted inside a `declare module '...' {` block)
declare module 'JSONStream' {
// Type definitions for JSONStream v0.8.0
// Project: https://github.com/dominictarr/JSONStream
// Definitions by: Bart van der Schoor <https://github.com/Bartvds>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference types="node" />
export interface Options {
recurse: boolean;
}
export function parse(pattern: any): NodeJS.ReadWriteStream;
export function parse(patterns: any[]): NodeJS.ReadWriteStream;
/**
* Create a writable stream.
* you may pass in custom open, close, and seperator strings. But, by default,
* JSONStream.stringify() will create an array,
* (with default options open='[\n', sep='\n,\n', close='\n]\n')
*/
export function stringify(): NodeJS.ReadWriteStream;
/** If you call JSONStream.stringify(false) the elements will only be seperated by a newline. */
export function stringify(
newlineOnly: NewlineOnlyIndicator,
): NodeJS.ReadWriteStream;
type NewlineOnlyIndicator = false;
/**
* Create a writable stream.
* you may pass in custom open, close, and seperator strings. But, by default,
* JSONStream.stringify() will create an array,
* (with default options open='[\n', sep='\n,\n', close='\n]\n')
*/
export function stringify(
open: string,
sep: string,
close: string,
): NodeJS.ReadWriteStream;
export function stringifyObject(): NodeJS.ReadWriteStream;
export function stringifyObject(
open: string,
sep: string,
close: string,
): NodeJS.ReadWriteStream;
}

View File

@ -21,6 +21,8 @@ declare module 'capitano' {
help: string;
options?: OptionDefinition[];
permission?: 'user';
root?: boolean;
primary?: boolean;
action(params: P, options: O, done: () => void): void;
}

12
typings/color-hash.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
declare module 'color-hash' {
interface Hasher {
hex(text: string): string;
}
class ColorHash {
hex(text: string): string;
rgb(text: string): [number, number, number];
}
export = ColorHash;
}

13
typings/dockerfile-template.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare module 'dockerfile-template' {
/**
* Variables which define what will be replaced, and what they will be replaced with.
*/
export interface TemplateVariables {
[key: string]: string;
}
export function process(
content: string,
variables: TemplateVariables,
): string;
}

View File

@ -1 +0,0 @@
declare module 'resin-cli-errors';

View File

@ -1,5 +1,5 @@
declare module 'resin-image-fs' {
import Promise = require('bluebird');
export function read(options: {}): Promise<NodeJS.ReadableStream>;
export function readFile(options: {}): Promise<string>;
}

View File

@ -1,5 +1,801 @@
declare module 'resin-sdk-preconfigured' {
import { ResinSDK } from 'resin-sdk';
let sdk: ResinSDK;
export = sdk;
import * as Promise from 'bluebird';
import { EventEmitter } from 'events';
import * as ResinErrors from 'resin-errors';
import { Readable } from 'stream';
/* tslint:disable:no-namespace */
namespace Pine {
// based on https://github.com/resin-io/pinejs-client-js/blob/master/core.d.ts
type RawFilter =
| string
| Array<string | Filter<any>>
| { $string: string; [index: string]: Filter<any> | string };
type Lambda<T> = {
$alias: string;
$expr: Filter<T>;
};
type OrderByValues = 'asc' | 'desc';
type OrderBy = string | string[] | { [index: string]: OrderByValues };
type ResourceObjFilter<T> = { [k in keyof T]?: object | number | string };
interface FilterArray<T> extends Array<Filter<T>> {}
type FilterExpressions<T> = {
$raw?: RawFilter;
$?: string | string[];
$and?: Filter<T> | FilterArray<T>;
$or?: Filter<T> | FilterArray<T>;
$in?: Filter<T> | FilterArray<T>;
$not?: Filter<T> | FilterArray<T>;
$any?: Lambda<T>;
$all?: Lambda<T>;
};
type Filter<T> = ResourceObjFilter<T> & FilterExpressions<T>;
type BaseExpandFor<T> = { [k in keyof T]?: object } | keyof T;
export type Expand<T> = BaseExpandFor<T> | Array<BaseExpandFor<T>>;
}
namespace ResinRequest {
interface ResinRequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
url: string;
apiKey?: string;
body?: any;
}
interface ResinRequestResponse extends Response {
body: any;
}
interface ResinRequest {
send: (options: ResinRequestOptions) => Promise<ResinRequestResponse>;
}
}
namespace ResinSdk {
interface Interceptor {
request?(response: any): Promise<any>;
response?(response: any): Promise<any>;
requestError?(error: Error): Promise<any>;
responseError?(error: Error): Promise<any>;
}
interface Config {
deployment: string | null;
deviceUrlsBase: string;
adminUrl: string;
apiUrl: string;
actionsUrl: string;
gitServerUrl: string;
pubnub: {
subscribe_key: string;
publish_key: string;
};
ga?: GaConfig;
mixpanelToken?: string;
intercomAppId?: string;
recurlyPublicKey?: string;
deviceTypes: DeviceType[];
DEVICE_ONLINE_ICON: string;
DEVICE_OFFLINE_ICON: string;
signupCodeRequired: boolean;
supportedSocialProviders: string[];
}
interface GaConfig {
site: string;
id: string;
}
interface DeviceType {
slug: string;
name: string;
arch: string;
state?: string;
isDependent?: boolean;
instructions?: string[] | DeviceTypeInstructions;
gettingStartedLink?: string | DeviceTypeGettingStartedLink;
stateInstructions?: { [key: string]: string[] };
options?: DeviceTypeOptions[];
initialization?: {
options?: DeviceInitializationOptions[];
operations: Array<{
command: string;
}>;
};
supportsBlink?: boolean;
yocto: {
fstype?: string;
deployArtifact: string;
};
}
interface DeviceTypeInstructions {
linux: string[];
osx: string[];
windows: string[];
}
interface DeviceTypeGettingStartedLink {
linux: string;
osx: string;
windows: string;
[key: string]: string;
}
interface DeviceTypeOptions {
options: DeviceTypeOptionsGroup[];
collapsed: boolean;
isCollapsible: boolean;
isGroup: boolean;
message: string;
name: string;
}
interface DeviceInitializationOptions {
message: string;
type: string;
name: string;
}
interface DeviceTypeOptionsGroup {
default: number | string;
message: string;
name: string;
type: string;
min?: number;
choices?: string[] | number[];
choicesLabels?: { [key: string]: string };
}
interface WithId {
id: number;
}
interface PineParams {
resource: string;
id?: number;
body?: object;
options?: PineOptions;
}
interface PineOptions {
filter?: object;
expand?: object | string;
orderBy?: Pine.OrderBy;
top?: string;
skip?: string;
select?: string | string[];
}
interface PineParamsFor<T> extends PineParams {
body?: Partial<T>;
options?: PineOptionsFor<T>;
}
interface PineParamsWithIdFor<T> extends PineParamsFor<T> {
id: number;
}
type PineFilterFor<T> = Pine.Filter<T>;
type PineExpandFor<T> = Pine.Expand<T>;
interface PineOptionsFor<T> extends PineOptions {
filter?: PineFilterFor<T>;
expand?: PineExpandFor<T>;
select?: Array<keyof T> | keyof T;
}
interface PineDeferred {
__id: number;
}
/**
* When not selected-out holds a deferred.
* When expanded hold an array with a single element.
*/
type NavigationResource<T = WithId> = T[] | PineDeferred;
/**
* When expanded holds an array, otherwise the property is not present.
* Selecting is not suggested,
* in that case it holds a deferred to the original resource.
*/
type ReverseNavigationResource<T = WithId> = T[] | undefined;
interface SocialServiceAccount {
provider: string;
display_name: string;
created_at: string;
id: number;
remote_id: string;
}
interface User {
id: number;
username: string;
email?: string;
first_name?: string;
last_name?: string;
company?: string;
account_type?: string;
has_disabled_newsletter?: boolean;
jwt_secret: string;
created_at: string;
twoFactorRequired?: boolean;
hasPasswordSet?: boolean;
needsPasswordReset?: boolean;
public_key?: boolean;
features?: string[];
intercomUserName?: string;
intercomUserHash?: string;
permissions?: string[];
loginAs?: boolean;
actualUser?: number;
// this is what the api route returns
social_service_account: ReverseNavigationResource<SocialServiceAccount>;
}
interface Application {
app_name: string;
device_type: string;
git_repository: string;
commit: string;
id: number;
device_type_info?: any;
has_dependent?: boolean;
should_track_latest_release: boolean;
user: NavigationResource<User>;
application_tag: ReverseNavigationResource<ApplicationTag>;
}
type BuildStatus =
| 'cancelled'
| 'error'
| 'interrupted'
| 'local'
| 'running'
| 'success'
| 'timeout'
| null;
interface Build {
log: string;
commit_hash: string;
created_at: string;
end_timestamp: string;
id: number;
message: string | null;
project_type: string;
push_timestamp: string | null;
start_timestamp: string;
status: BuildStatus;
update_timestamp: string | null;
}
interface BillingAccountAddressInfo {
address1: string;
address2: string;
city: string;
state: string;
zip: string;
country: string;
phone: string;
}
interface BillingAccountInfo {
account_state: string;
first_name: string;
last_name: string;
company_name: string;
cc_emails: string;
vat_number: string;
address: BillingAccountAddressInfo;
}
type BillingInfoType = 'bank_account' | 'credit_card' | 'paypal';
interface BillingInfo {
full_name: string;
first_name: string;
last_name: string;
company: string;
vat_number: string;
address1: string;
address2: string;
city: string;
state: string;
zip: string;
country: string;
phone: string;
type?: BillingInfoType;
}
interface CardBillingInfo extends BillingInfo {
card_type: string;
year: string;
month: string;
first_one: string;
last_four: string;
}
interface BankAccountBillingInfo extends BillingInfo {
account_type: string;
last_four: string;
name_on_account: string;
routing_number: string;
}
interface TokenBillingSubmitInfo {
token_id: string;
}
interface BillingPlanInfo {
name: string;
billing?: BillingPlanBillingInfo;
}
interface BillingPlanBillingInfo {
currency: string;
currencySymbol?: string;
}
interface InvoiceInfo {
closed_at: string;
created_at: string;
currency: string;
invoice_number: string;
subtotal_in_cents: string;
total_in_cents: string;
uuid: string;
}
interface Device {
created_at: string;
device_type: string;
id: number;
name: string;
os_version: string;
os_variant?: string;
status_sort_index?: number;
uuid: string;
ip_address: string | null;
vpn_address: string | null;
last_connectivity_event: string;
is_in_local_mode?: boolean;
app_name?: string;
state?: { key: string; name: string };
status: string;
provisioning_state: string;
is_online: boolean;
is_connected_to_vpn: boolean;
supervisor_version: string;
is_web_accessible: boolean;
has_dependent: boolean;
note: string;
location: string;
latitude?: string;
longitude?: string;
custom_latitude?: string;
custom_longitude?: string;
download_progress?: number;
provisioning_progress?: number;
local_id?: string;
device_environment_variable: ReverseNavigationResource<
DeviceEnvironmentVariable
>;
device_tag: ReverseNavigationResource<DeviceTag>;
}
interface LogMessage {
message: string;
isSystem: boolean;
timestamp: number | null;
serviceId: number | null;
}
interface LogsSubscription extends EventEmitter {
unsubscribe(): void;
}
interface SSHKey {
title: string;
public_key: string;
id: number;
created_at: string;
}
type ImgConfigOptions = {
network?: 'ethernet' | 'wifi';
appUpdatePollInterval?: number;
wifiKey?: string;
wifiSsid?: string;
ip?: string;
gateway?: string;
netmask?: string;
version?: string;
};
type OsVersions = {
latest: string;
recommended: string;
default: string;
versions: string[];
};
interface EnvironmentVariableBase {
id: number;
name: string;
value: string;
}
interface EnvironmentVariable extends EnvironmentVariableBase {
application: NavigationResource<Application>;
}
interface DeviceEnvironmentVariable extends EnvironmentVariableBase {
env_var_name?: string;
device: NavigationResource<Device>;
}
interface ResourceTagBase {
id: number;
tag_key: string;
value: string;
}
interface ApplicationTag extends ResourceTagBase {
application: NavigationResource<Application>;
}
interface DeviceTag extends ResourceTagBase {
device: NavigationResource<Device>;
}
type LogsPromise = Promise<LogMessage[]>;
interface ResinSDK {
auth: {
register: (
credentials: { email: string; password: string },
) => Promise<string>;
authenticate: (
credentials: { email: string; password: string },
) => Promise<string>;
login: (
credentials: { email: string; password: string },
) => Promise<void>;
loginWithToken: (authToken: string) => Promise<void>;
logout: () => Promise<void>;
getToken: () => Promise<string>;
whoami: () => Promise<string | undefined>;
isLoggedIn: () => Promise<boolean>;
getUserId: () => Promise<number>;
getEmail: () => Promise<string>;
twoFactor: {
isEnabled: () => Promise<boolean>;
isPassed: () => Promise<boolean>;
challenge: (code: string) => Promise<void>;
};
};
settings: {
get(key: string): Promise<string>;
getAll(): Promise<{ [key: string]: string }>;
};
request: ResinRequest.ResinRequest;
errors: {
ResinAmbiguousApplication: ResinErrors.ResinAmbiguousApplication;
ResinAmbiguousDevice: ResinErrors.ResinAmbiguousDevice;
ResinApplicationNotFound: ResinErrors.ResinApplicationNotFound;
ResinBuildNotFound: ResinErrors.ResinBuildNotFound;
ResinDeviceNotFound: ResinErrors.ResinDeviceNotFound;
ResinExpiredToken: ResinErrors.ResinExpiredToken;
ResinInvalidDeviceType: ResinErrors.ResinInvalidDeviceType;
ResinInvalidParameterError: ResinErrors.ResinInvalidParameterError;
ResinKeyNotFound: ResinErrors.ResinKeyNotFound;
ResinMalformedToken: ResinErrors.ResinMalformedToken;
ResinNotLoggedIn: ResinErrors.ResinNotLoggedIn;
ResinRequestError: ResinErrors.ResinRequestError;
ResinSupervisorLockedError: ResinErrors.ResinSupervisorLockedError;
};
models: {
application: {
create(
name: string,
deviceType: string,
parentNameOrId?: number | string,
): Promise<Application>;
get(
nameOrId: string | number,
options?: PineOptionsFor<Application>,
): Promise<Application>;
getAppByOwner(
appName: string,
owner: string,
options?: PineOptionsFor<Application>,
): Promise<Application>;
getAll(options?: PineOptionsFor<Application>): Promise<Application[]>;
has(name: string): Promise<boolean>;
hasAny(): Promise<boolean>;
remove(nameOrId: string | number): Promise<void>;
restart(nameOrId: string | number): Promise<void>;
enableDeviceUrls(nameOrId: string | number): Promise<void>;
disableDeviceUrls(nameOrId: string | number): Promise<void>;
grantSupportAccess(
nameOrId: string | number,
expiryTimestamp: number,
): Promise<void>;
revokeSupportAccess(nameOrId: string | number): Promise<void>;
reboot(appId: number, { force }: { force?: boolean }): Promise<void>;
shutdown(
appId: number,
{ force }: { force?: boolean },
): Promise<void>;
purge(appId: number): Promise<void>;
generateApiKey(nameOrId: string | number): Promise<string>;
generateProvisioningKey(nameOrId: string | number): Promise<string>;
tags: {
getAllByApplication(
nameOrId: string | number,
options?: PineOptionsFor<ApplicationTag>,
): Promise<ApplicationTag[]>;
getAll(
options?: PineOptionsFor<ApplicationTag>,
): Promise<ApplicationTag[]>;
set(
nameOrId: string | number,
tagKey: string,
value: string,
): Promise<void>;
remove(nameOrId: string | number, tagKey: string): Promise<void>;
};
};
build: {
get(id: number, options?: PineOptionsFor<Build>): Promise<Build>;
getAllByApplication(
nameOrId: string | number,
options?: PineOptionsFor<Build>,
): Promise<Build[]>;
};
billing: {
getAccount(): Promise<BillingAccountInfo>;
getPlan(): Promise<BillingPlanInfo>;
getBillingInfo(): Promise<BillingInfo>;
updateBillingInfo(
billingInfo: TokenBillingSubmitInfo,
): Promise<BillingInfo>;
getInvoices(): Promise<InvoiceInfo[]>;
downloadInvoice(invoiceNumber: string): Promise<Blob>;
};
device: {
get(
uuidOrId: string | number,
options?: PineOptionsFor<Device>,
): Promise<Device>;
getByName(
nameOrId: string | number,
options?: PineOptionsFor<Device>,
): Promise<Device[]>;
getAll(options?: PineOptionsFor<Device>): Promise<Device[]>;
getAllByApplication(
nameOrId: string | number,
options?: PineOptionsFor<Device>,
): Promise<Device[]>;
getAllByParentDevice(
parentUuidOrId: string | number,
options?: PineOptionsFor<Device>,
): Promise<Device[]>;
getName(uuidOrId: string | number): Promise<string>;
getApplicationName(uuidOrId: string | number): Promise<string>;
getApplicationInfo(
uuidOrId: string | number,
): Promise<{
appId: string;
commit: string;
containerId: string;
env: { [key: string]: string | number };
imageId: string;
}>;
has(uuidOrId: string | number): Promise<boolean>;
isOnline(uuidOrId: string | number): Promise<boolean>;
getLocalIPAddressess(uuidOrId: string | number): Promise<string[]>;
getDashboardUrl(uuid: string): string;
getSupportedDeviceTypes(): Promise<string[]>;
getManifestBySlug(slugOrName: string): Promise<DeviceType>;
getManifestByApplication(
nameOrId: string | number,
): Promise<DeviceType>;
move(
uuidOrId: string | number,
applicationNameOrId: string | number,
): Promise<void>;
note(uuidOrId: string | number, note: string): Promise<void>;
remove(uuidOrId: string | number): Promise<void>;
rename(uuidOrId: string | number, newName: string): Promise<void>;
setCustomLocation(
uuidOrId: string | number,
location: { latitude: number; longitude: number },
): Promise<void>;
unsetCustomLocation(uuidOrId: string | number): Promise<void>;
identify(uuidOrId: string | number): Promise<void>;
startApplication(uuidOrId: string | number): Promise<void>;
stopApplication(uuidOrId: string | number): Promise<void>;
restartApplication(uuidOrId: string | number): Promise<void>;
grantSupportAccess(
uuidOrId: string | number,
expiryTimestamp: number,
): Promise<void>;
revokeSupportAccess(uuidOrId: string | number): Promise<void>;
reboot(
uuidOrId: string | number,
{ force }?: { force?: boolean },
): Promise<void>;
shutdown(
uuidOrId: string | number,
{ force }?: { force?: boolean },
): Promise<void>;
purge(uuidOrId: string | number): Promise<void>;
update(
uuidOrId: string | number,
{ force }?: { force?: boolean },
): Promise<void>;
getDisplayName(deviceTypeName: string): string;
getDeviceSlug(deviceTypeName: string): string;
generateUniqueKey(): string;
register(
applicationNameOrId: string | number,
uuid?: string,
): Promise<object>;
generateDeviceKey(uuidOrId: string | number): Promise<string>;
enableDeviceUrl(uuidOrId: string | number): Promise<void>;
disableDeviceUrl(uuidOrId: string | number): Promise<void>;
hasDeviceUrl(uuidOrId: string | number): Promise<boolean>;
getDeviceUrl(uuidOrId: string | number): Promise<string>;
enableTcpPing(uuidOrId: string | number): Promise<void>;
disableTcpPing(uuidOrId: string | number): Promise<void>;
ping(uuidOrId: string | number): Promise<void>;
getStatus(device: object): string;
lastOnline(device: Device): string;
tags: {
getAllByApplication(
nameOrId: string | number,
options?: PineOptionsFor<DeviceTag>,
): Promise<DeviceTag[]>;
getAllByDevice(
uuidOrId: string | number,
options?: PineOptionsFor<DeviceTag>,
): Promise<DeviceTag[]>;
getAll(options?: PineOptionsFor<DeviceTag>): Promise<DeviceTag[]>;
set(
uuidOrId: string | number,
tagKey: string,
value: string,
): Promise<void>;
remove(uuidOrId: string | number, tagKey: string): Promise<void>;
};
};
environmentVariables: {
device: {
getAll(id: number): Promise<DeviceEnvironmentVariable[]>;
getAllByApplication(
applicationNameOrId: number | string,
): Promise<DeviceEnvironmentVariable[]>;
update(id: number, value: string): Promise<void>;
create(
uuidOrId: number | string,
name: string,
value: string,
): Promise<void>;
remove(id: number): Promise<void>;
};
getAllByApplication(
applicationNameOrId: number | string,
): Promise<EnvironmentVariable[]>;
update(id: number, value: string): Promise<void>;
create(
applicationNameOrId: number | string,
name: string,
value: string,
): Promise<void>;
remove(id: number): Promise<void>;
isSystemVariable(variable: { name: string }): boolean;
};
config: {
getAll: () => Promise<Config>;
getDeviceTypes: () => Promise<DeviceType[]>;
getDeviceOptions(
deviceType: string,
): Promise<Array<DeviceTypeOptions | DeviceInitializationOptions>>;
};
key: {
getAll(options?: PineOptionsFor<SSHKey>): Promise<SSHKey[]>;
get(id: string | number): Promise<SSHKey>;
remove(id: string | number): Promise<void>;
create(title: string, key: string): Promise<SSHKey>;
};
os: {
getConfig(
nameOrId: string | number,
options?: ImgConfigOptions,
): Promise<object>;
getDownloadSize(slug: string, version?: string): Promise<number>;
getSupportedVersions(slug: string): Promise<OsVersions>;
getMaxSatisfyingVersion(
deviceType: string,
versionOrRange: string,
): string;
getLastModified(deviceType: string, version?: string): Promise<Date>;
download(deviceType: string, version?: string): Promise<Readable>;
};
};
logs: {
history(uuid: string): LogsPromise;
historySinceLastClear(uuid: string): LogsPromise;
subscribe(uuid: string): Promise<LogsSubscription>;
clear(uuid: string): void;
};
pine: {
delete<T>(
params: PineParamsWithIdFor<T> | PineParamsFor<T>,
): Promise<string>;
get<T>(params: PineParamsWithIdFor<T>): Promise<T>;
get<T>(params: PineParamsFor<T>): Promise<T[]>;
get<T, Result>(params: PineParamsFor<T>): Promise<Result>;
post<T>(params: PineParams): Promise<T>;
patch<T>(params: PineParamsWithIdFor<T>): Promise<T>;
};
interceptors: Interceptor[];
}
}
interface SdkOptions {
apiUrl?: string;
/**
* @deprecated Use resin.auth.loginWithToken(apiKey) instead
*/
apiKey?: string;
imageMakerUrl?: string;
dataDirectory?: string;
isBrowser?: boolean;
debug?: boolean;
}
interface SdkConstructor {
(options?: SdkOptions): ResinSdk.ResinSDK;
setSharedOptions(options: SdkOptions): void;
fromSharedOptions: () => ResinSdk.ResinSDK;
}
const ResinSdk: ResinSdk.ResinSDK;
export = ResinSdk;
}