Compare commits

...

247 Commits

Author SHA1 Message Date
Girish Ramakrishnan 26aefadfba systemd: fix crashnotifier 2015-09-07 21:40:01 -07:00
Girish Ramakrishnan 51a28842cf systemd: pass the instance name as argument 2015-09-07 21:16:22 -07:00
Girish Ramakrishnan 210c2f3cc1 Output some logs in crashnotifier 2015-09-07 21:10:00 -07:00
Girish Ramakrishnan 773c326eb7 systemd: just wait for 5 seconds for box to die 2015-09-07 20:58:14 -07:00
Girish Ramakrishnan cb2fb026c5 systemd: do not restart crashnotifier 2015-09-07 20:54:58 -07:00
Girish Ramakrishnan a4731ad054 200m is a more sane memory limit 2015-09-07 20:48:29 -07:00
Girish Ramakrishnan aa33938fb5 systemd: fix config files 2015-09-07 20:46:32 -07:00
Girish Ramakrishnan edfe8f1ad0 disable pager when collecting logs 2015-09-07 20:27:27 -07:00
Girish Ramakrishnan 41399a2593 Make crashnotifier.js executable 2015-09-07 20:15:13 -07:00
Girish Ramakrishnan 2a4c467ab8 systemd: Fix crashnotifier 2015-09-07 20:14:37 -07:00
Girish Ramakrishnan 6be6092c0e Add memory limits on services 2015-09-07 19:16:34 -07:00
Girish Ramakrishnan e76584b0da Move from supervisor to systemd
This removes logrotate as well since we use systemd logging
2015-09-07 14:31:25 -07:00
Girish Ramakrishnan b3816615db run upto 5 apptasks in parallel
fixes #482
2015-09-05 09:17:46 -07:00
Johannes Zellner 212d0bd55a Revert "Add hack for broken app backup tarballs"
This reverts commit 9723951bfc.
2015-08-31 21:44:24 -07:00
Girish Ramakrishnan 712ada940e Add hack for broken app backup tarballs 2015-08-31 18:58:38 -07:00
Johannes Zellner ba690c6346 Add missing records argument 2015-08-30 23:00:01 -07:00
Johannes Zellner e910e19f57 Fix debug tag 2015-08-30 22:54:52 -07:00
Johannes Zellner 0c2532b0b5 Give default value to config.dnsInSync 2015-08-30 22:35:44 -07:00
Johannes Zellner 9c9b17a5f0 Remove cloudron.config prior to every test run 2015-08-30 22:35:44 -07:00
Johannes Zellner 816dea91ec Assert for dns record values 2015-08-30 22:35:44 -07:00
Johannes Zellner c228f8d4d5 Merge admin dns and mail dns setup
This now also checks if the mail records are in sync
2015-08-30 22:35:43 -07:00
Johannes Zellner 05bb99fad4 give dns record changeIds as a result for addMany() 2015-08-30 22:35:43 -07:00
Johannes Zellner 51b2457b3d Setup webadmin domain on the box side 2015-08-30 22:35:43 -07:00
Girish Ramakrishnan ed71fca23e Fix css 2015-08-30 22:25:18 -07:00
Girish Ramakrishnan 20e8e72ac2 reserved blocks are used 2015-08-30 22:24:57 -07:00
Girish Ramakrishnan 13fe0eb882 Only display one donut for memory usage 2015-08-30 22:13:01 -07:00
Girish Ramakrishnan e0476c9030 Reboot is a post route 2015-08-30 21:38:54 -07:00
Girish Ramakrishnan fca82fd775 Display upto 600mb for apps 2015-08-30 17:21:44 -07:00
Johannes Zellner 37c8ba8ddd Reduce logging for aws credentials 2015-08-30 17:03:10 -07:00
Johannes Zellner f87011b5c2 Also always check for dns propagation 2015-08-30 17:00:23 -07:00
Johannes Zellner 7f149700f8 Remove wrong optimization for subdomain records 2015-08-30 16:54:33 -07:00
Johannes Zellner 78ba9070fc use config.appFqdn() to handle custom domains 2015-08-30 16:29:09 -07:00
Johannes Zellner e31e5e1f69 Reuse dnsRecordId for record status id 2015-08-30 15:58:54 -07:00
Johannes Zellner 31d9027677 Query dns status with aws statusId 2015-08-30 15:51:33 -07:00
Johannes Zellner debcd6f353 aws provides uppercase properties 2015-08-30 15:47:08 -07:00
Johannes Zellner 5cb1681922 Fixup the zonename comparison 2015-08-30 15:37:18 -07:00
Johannes Zellner 9074bccea0 Move subdomain management from appstore to box 2015-08-30 15:29:14 -07:00
Girish Ramakrishnan 291798f574 Pass along aws config for updates 2015-08-27 22:45:04 -07:00
Girish Ramakrishnan b104843ae1 Add missing quotes to cloudron.conf 2015-08-27 20:15:04 -07:00
Girish Ramakrishnan dd062c656f Fix failing test 2015-08-27 11:43:36 -07:00
Girish Ramakrishnan ae2eb718c6 check if response has credentials object 2015-08-27 11:43:02 -07:00
Girish Ramakrishnan 7ac26bb653 Fix backup response 2015-08-27 11:19:40 -07:00
Girish Ramakrishnan 41a726e8a7 Fix backup test 2015-08-27 11:17:36 -07:00
Girish Ramakrishnan 4b69216548 bash: quote the array expansion 2015-08-27 10:13:05 -07:00
Girish Ramakrishnan 99395ddf5a bash: quoting array expansion because thats how it is 2015-08-27 09:49:44 -07:00
Girish Ramakrishnan 5f9fa5c352 bash: empty array expansion barfs with set -u 2015-08-27 09:33:40 -07:00
Girish Ramakrishnan 9013331917 Fix coding style 2015-08-27 09:30:32 -07:00
Girish Ramakrishnan 3a8f80477b getSignedDownloadUrl must return an object with url and sessionToken 2015-08-27 09:26:19 -07:00
Johannes Zellner 813c680ed0 pass full box data to the update 2015-08-26 10:59:17 -07:00
Johannes Zellner a0eccd615f Send new version to update to to the installer 2015-08-26 09:42:48 -07:00
Johannes Zellner 59be539ecd make restoreapp.sh support aws session tokens 2015-08-26 09:14:15 -07:00
Johannes Zellner a04740114c Generate app restore urls locally 2015-08-26 09:11:28 -07:00
Johannes Zellner 60b5d71c74 appBackupIds are not needed for backup url generation 2015-08-26 09:06:45 -07:00
Johannes Zellner 0a8b4b0c43 Load our style sheet as early as possible 2015-08-25 21:59:01 -07:00
Johannes Zellner ec21105c47 use backupKey from userData 2015-08-25 18:44:52 -07:00
Girish Ramakrishnan 444258e7ee backupKey is a function 2015-08-25 18:37:51 -07:00
Johannes Zellner e6fd05c2bd Support optional aws related userData 2015-08-25 17:52:01 -07:00
Johannes Zellner 9fdcd452d0 Use locally generate signed urls for app backup 2015-08-25 17:52:01 -07:00
Johannes Zellner f39b9d5618 Support session tokens in backupapp.sh 2015-08-25 17:52:00 -07:00
Johannes Zellner 76e4c4919d Only federated tokens need session token 2015-08-25 17:52:00 -07:00
Johannes Zellner d1f159cdb4 Also send the restoreKey for the backup done webhook 2015-08-25 17:52:00 -07:00
Johannes Zellner c63065e460 Also send the sessionToken when using the pre-signed url 2015-08-25 17:52:00 -07:00
Johannes Zellner 124c1d94a4 Translate the federated credentials 2015-08-25 17:52:00 -07:00
Johannes Zellner e9161b726a AWS credential creation returns 201 2015-08-25 17:52:00 -07:00
Johannes Zellner fd0d27b192 AWS credentials are now dealt with a level down 2015-08-25 17:52:00 -07:00
Johannes Zellner 50064a40fe Use dev bucket for now as a default 2015-08-25 17:52:00 -07:00
Johannes Zellner c9bc5fc38e Use signed urls for upload on the box side 2015-08-25 17:52:00 -07:00
Johannes Zellner 58f533fe50 Add config.aws().backupPrefix 2015-08-25 17:52:00 -07:00
Johannes Zellner efcdffd8ff Add getSignedUploadUrl() to aws.js 2015-08-25 17:52:00 -07:00
Johannes Zellner 22793c3886 move aws-sdk from dev to normal dependencies 2015-08-25 17:52:00 -07:00
Johannes Zellner 797ddbacc0 Return aws credentials from config.js 2015-08-25 17:52:00 -07:00
Johannes Zellner e011962469 refactor backupBoxWithAppBackupIds() 2015-08-25 17:52:00 -07:00
Johannes Zellner b376ad9815 Add webhooks.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 77248fe65c Construct backupUrl locally 2015-08-25 17:51:59 -07:00
Johannes Zellner 1dad115203 Add initial aws object to config.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 8812d58031 Add backupKey to config 2015-08-25 17:51:59 -07:00
Johannes Zellner fff7568f7e Add aws.js 2015-08-25 17:51:59 -07:00
Johannes Zellner ff6662579d Fix typo in backupapp.sh help output 2015-08-25 17:51:59 -07:00
Girish Ramakrishnan 0cf9fbd909 Merge data into args 2015-08-25 15:55:52 -07:00
Girish Ramakrishnan 848b745fcb Fix boolean logic 2015-08-25 12:24:02 -07:00
Girish Ramakrishnan 9a35c40b24 Add force argument
This fixes crash when auto-updating apps
2015-08-25 10:01:20 -07:00
Girish Ramakrishnan 1f1e6124cd oldConfig can be null during a restore/upgrade 2015-08-25 09:59:44 -07:00
Girish Ramakrishnan 033df970ad Update manifestformat@1.7.0 2015-08-24 22:56:02 -07:00
Girish Ramakrishnan dd80a795a0 Read memoryLimit from manifest 2015-08-24 22:44:35 -07:00
Girish Ramakrishnan 1eec6a39c6 Show upto 200mb 2015-08-24 22:39:06 -07:00
Girish Ramakrishnan dd6b8face9 Set app memory limit to 200MB (includes 100 MB swap) 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan 288de7e03a Add RSTATE_ERROR 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan a760ef4d22 Rebase addons to use base image 0.3.3 2015-08-24 10:19:18 -07:00
Johannes Zellner 0dd745bce4 Fix form submit with enter for update form 2015-08-22 17:21:25 -07:00
Johannes Zellner d4d5d371ac Use POST heartbeat route instead of GET 2015-08-22 16:51:56 -07:00
Johannes Zellner 205bf4ddbd Offset the footer in apps view 2015-08-20 23:50:52 -07:00
Girish Ramakrishnan 4ab84d42c6 Delete image only if it changed
This optimization won't work if we have two dockerImage with same
image id....
2015-08-19 14:24:32 -07:00
Girish Ramakrishnan ee74badf3a Check for dockerImage in manifest in install/update/restore routes 2015-08-19 11:08:45 -07:00
Girish Ramakrishnan aa173ff74c restore without a backup is the same as re-install 2015-08-19 11:00:00 -07:00
Girish Ramakrishnan b584fc33f5 CN of admin group is admins 2015-08-18 16:35:52 -07:00
Girish Ramakrishnan 15c9d8682e Base image is now 0.3.3 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 361be8c26b containerId can be null 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 4db9a5edd6 Clean up the old image and not the current one 2015-08-18 10:01:15 -07:00
Johannes Zellner bcc878da43 Hide update input fields and update button if it is blocked by apps 2015-08-18 16:59:36 +02:00
Johannes Zellner 79f179fed4 Add note, why sendError() is required 2015-08-18 16:53:29 +02:00
Johannes Zellner a924a9a627 Revert "remove obsolete sendError() function"
This reverts commit 5d9b122dd5.
2015-08-18 16:49:53 +02:00
Girish Ramakrishnan 45d444df0e leave a note about force_update 2015-08-17 21:30:56 -07:00
Girish Ramakrishnan 92461a3366 Remove ununsed require 2015-08-17 21:23:32 -07:00
Girish Ramakrishnan 032a430c51 Fix debug message 2015-08-17 21:23:27 -07:00
Girish Ramakrishnan a6a3855e79 Do not remove icon for non-appstore installs
Fixes #466
2015-08-17 19:37:51 -07:00
Girish Ramakrishnan 2386545814 Add a note why oldConfig can be null 2015-08-17 10:05:07 -07:00
Johannes Zellner 2059152dd3 remove obsolete sendError() function 2015-08-17 14:55:56 +02:00
Johannes Zellner 32d2c260ab Move appstore badges out of the way for the app titles 2015-08-17 11:50:31 +02:00
Johannes Zellner 384c7873aa Correctly mark apps pending for approval
Fixes #339
2015-08-17 11:50:08 +02:00
Girish Ramakrishnan 9266302c4c Print graphite container id 2015-08-13 15:57:36 -07:00
Girish Ramakrishnan 755dce7bc4 fix graph issue finally 2015-08-13 15:54:27 -07:00
Girish Ramakrishnan dd3e38ae55 Use latest graphite 2015-08-13 15:53:36 -07:00
Girish Ramakrishnan 9dfaa2d20f Create symlink in start.sh (and not container setup) 2015-08-13 15:36:21 -07:00
Girish Ramakrishnan d6a4ff23e2 restart mysql in start.sh and not container setup 2015-08-13 15:16:01 -07:00
Girish Ramakrishnan c2ab7e2c1f restart collectd 2015-08-13 15:04:57 -07:00
Girish Ramakrishnan b9e4662dbb fix graphs again 2015-08-13 15:03:44 -07:00
Girish Ramakrishnan 10df0a527f Fix typo
remove thead_cache_size. it's dynamic anyways
2015-08-13 14:53:05 -07:00
Girish Ramakrishnan 9aad3688e1 Revert "Add hack to make graphs work with latest collectd"
This reverts commit a959418544.
2015-08-13 14:42:47 -07:00
Girish Ramakrishnan e78dbcb5d4 limit threads and max connections 2015-08-13 14:42:36 -07:00
Girish Ramakrishnan 5e8cd09f51 Bump infra version 2015-08-13 14:22:39 -07:00
Girish Ramakrishnan 22f65a9364 Add hack to make graphs work with latest collectd
For some reason df-vda1 is not being collected by carbon. I have tried
all sorts of things and nothing works. This is a hack to get it working.
2015-08-13 13:47:44 -07:00
Girish Ramakrishnan 81b7432044 Turn off performance_schema in mysql 5.6 2015-08-13 13:47:44 -07:00
Girish Ramakrishnan d49b90d9f2 Remove unused nodejs-disks 2015-08-13 10:34:06 -07:00
Girish Ramakrishnan 9face9cf35 systemd has moved around the cgroup hierarchy
https://github.com/docker/docker/issues/9902

There is some rationale here:
https://libvirt.org/cgroups.html
2015-08-13 10:21:33 -07:00
Girish Ramakrishnan 33ac34296e CpuShares is part of HostConfig 2015-08-12 23:47:35 -07:00
Girish Ramakrishnan 670ffcd489 Add warning 2015-08-12 19:52:23 -07:00
Girish Ramakrishnan ec7b365c31 Use BASE_IMAGE as well 2015-08-12 19:51:44 -07:00
Girish Ramakrishnan 433d78c7ff Fix graphite version 2015-08-12 19:51:08 -07:00
Girish Ramakrishnan ed041fdca6 Put image names in one place 2015-08-12 19:38:44 -07:00
Girish Ramakrishnan b8e4ed2369 Use latest images 2015-08-12 19:19:58 -07:00
Johannes Zellner d12f260d12 Prevent accessing oldConfig if it does not exist 2015-08-12 21:17:52 +02:00
Johannes Zellner ba7989b57b Add ldap 'users' group 2015-08-12 17:38:31 +02:00
Johannes Zellner 88df410f5b Add ldap search unit tests 2015-08-12 15:31:54 +02:00
Johannes Zellner 2436db3b1f Add ldap memberof attribute 2015-08-12 15:31:44 +02:00
Johannes Zellner d15874df63 Add initial ldap unit tests 2015-08-12 15:00:38 +02:00
Johannes Zellner 8fb90254cd Ensure the focus is properly set when restoring 2015-08-12 14:35:51 +02:00
Johannes Zellner cbd712c20e Better integrate the progress bar 2015-08-12 14:32:20 +02:00
Johannes Zellner 8c004798f2 Improve login form layout 2015-08-12 14:23:13 +02:00
Johannes Zellner c1b0cbe78d Give appstore hover a different color 2015-08-12 14:07:40 +02:00
Johannes Zellner 5ee72c8e98 Make webadmin pages a bit more streamlined with padding 2015-08-12 13:48:55 +02:00
Girish Ramakrishnan c125cc17dc Apps must only get 50% less cpu than system processes when there is a contention for cpu 2015-08-11 17:00:48 -07:00
Johannes Zellner 18feff1bfb Increase installed app title 2015-08-11 15:22:30 +02:00
Johannes Zellner f74f713bbd Hide geeky toolbar in apps icons 2015-08-11 13:04:50 +02:00
Girish Ramakrishnan 0ea14db172 Fix redis installation on 1.7 2015-08-10 23:00:24 -07:00
Girish Ramakrishnan 74785a40d5 r -> ro (docker 1.7) 2015-08-10 21:14:28 -07:00
Girish Ramakrishnan dcfcd5be84 Create docker volume directories since docker 1.7 does not create them 2015-08-10 21:00:56 -07:00
Girish Ramakrishnan 814674eac5 addons can be null in apps.backupApp
addons.backup already takes care of null.

a future commit will give defaults for all non-default manifest fields
at some point and document them as so
2015-08-10 13:47:51 -07:00
Girish Ramakrishnan 1a7fff9867 Keep linter happy 2015-08-10 13:42:04 -07:00
Johannes Zellner 30b248a0f6 Allow non published versions to be shown if explicitly requested
Fixes #468
2015-08-10 16:16:40 +02:00
Johannes Zellner 7168455de3 Do not use table layout for login view
Fixes #458
2015-08-10 15:26:45 +02:00
Johannes Zellner 085f63e3c7 Show cloudron name in login screen 2015-08-10 15:04:12 +02:00
Johannes Zellner 015be64923 Show cloudron avatar in login screen 2015-08-10 15:01:58 +02:00
Johannes Zellner 2c2471811d Restructure the login page 2015-08-10 14:51:04 +02:00
Johannes Zellner 1025249e93 Since addons are optional, ensure we have a valid empty object in the db 2015-08-10 10:37:55 +02:00
Johannes Zellner 41ffc4bcf3 If we have an empty app search show modal dialog link 2015-08-09 15:19:21 +02:00
Johannes Zellner 2739d54cc1 Make appstore feedback form a modal dialog 2015-08-09 14:48:00 +02:00
Girish Ramakrishnan c4c463cbc2 collect logs using a sudo script
docker logs can only be read by root
2015-08-08 19:04:59 -07:00
Girish Ramakrishnan 8cd13bd43f Update safetydance 2015-08-08 18:53:16 -07:00
Girish Ramakrishnan e4ef279759 Update safetydance and lastmile 2015-08-06 13:54:15 -07:00
Girish Ramakrishnan cf7fecb57b bump cloudron-manifestformat 2015-08-06 13:50:27 -07:00
Girish Ramakrishnan 226041dcb1 Display settings path
Fixes #465
2015-08-06 13:44:09 -07:00
Johannes Zellner 7548025561 If an app search is empty, show hint to give feedback 2015-08-06 18:35:08 +02:00
Johannes Zellner fdbee427ee Show app feedback form in appstore
Fixes #461
2015-08-06 18:30:49 +02:00
Johannes Zellner d861d6d6e4 Properly offset the footer in support view 2015-08-06 18:30:25 +02:00
Johannes Zellner 0a648edcaa Add app feedback category 2015-08-06 17:34:40 +02:00
Johannes Zellner 18850c1fba Cloudron prices are in cents 2015-08-06 16:24:19 +02:00
Girish Ramakrishnan f6df4cab67 Remove ADMIN_ORIGIN 2015-08-05 17:27:55 -07:00
Johannes Zellner 019d29c5b7 Use assert.strictEqual() to see the values 2015-08-05 17:49:19 +02:00
Johannes Zellner 0b4256a992 Unify feedback and ticket forms 2015-08-05 14:27:04 +02:00
Johannes Zellner 7d58d69389 Fix setup step on ng-enter 2015-08-04 22:17:58 +02:00
Johannes Zellner 864dd5bf26 New shrinkwrap for ldapjs without dtrace-provider
We have to install ldapjs with --no-optional

Fixes #460
2015-08-04 20:43:36 +02:00
Johannes Zellner abdde7a950 Put the correct faq and docs links 2015-08-04 19:36:05 +02:00
Johannes Zellner 8bcbd860be Add unit tests for feedback route and fix the route 2015-08-04 16:59:35 +02:00
Johannes Zellner be61c42fe8 Send feedback and tickets to support@cloudron.io 2015-08-04 16:05:20 +02:00
Johannes Zellner 6d5afc2d75 Give support form headers more space 2015-08-04 16:04:44 +02:00
Johannes Zellner 88d905e8cc Add support form feedback 2015-08-04 16:01:50 +02:00
Johannes Zellner d8ccc766b9 Add text-bold class 2015-08-04 16:01:33 +02:00
Johannes Zellner d22e0f0483 mailer functions only enqueue, respond immediately 2015-08-04 15:39:14 +02:00
Johannes Zellner c8f6973312 Do not send adminEmail for feedback mails 2015-08-04 14:56:43 +02:00
Johannes Zellner 3f0f0048bc add missing email format 2015-08-04 14:52:40 +02:00
Johannes Zellner 88643f0875 Add missing %> 2015-08-04 14:49:43 +02:00
Johannes Zellner e11bb10bb8 The requested function is in mailer 2015-08-04 14:45:42 +02:00
Johannes Zellner 7b9930c7f0 Do the feedback and ticket form plumbing 2015-08-04 14:44:39 +02:00
Johannes Zellner da48e32bcc Add feedback route 2015-08-04 14:31:40 +02:00
Johannes Zellner 57e2803bd2 Add feedback email template 2015-08-04 14:31:33 +02:00
Johannes Zellner 0d1ba01d65 Add initial support view 2015-08-04 11:33:36 +02:00
Girish Ramakrishnan 95cbec19af Copy the manifest because changes are made to it
Because of that, manifest verification fails (isNew property appears in manfiest)
2015-08-03 21:31:15 -07:00
Girish Ramakrishnan cc97654b23 Fix text 2015-08-02 19:02:45 -07:00
Girish Ramakrishnan 5bb983f175 Send docker log in crash email 2015-08-01 21:42:34 -07:00
Johannes Zellner 7cb6434de1 Move avatar name below the selected avatar preview 2015-07-30 16:38:10 +02:00
Johannes Zellner cb1b495da2 Revert "Actually remove dtrace dep"
This reverts commit 2b9bf6d019.
2015-07-30 14:53:53 +02:00
Girish Ramakrishnan e134136d59 previewAvatar seems to be defined in step1 and step2 2015-07-29 18:10:25 -07:00
Girish Ramakrishnan 85a681e330 There is no step4 2015-07-29 17:09:05 -07:00
Girish Ramakrishnan dc5c0fd830 setPreviewAvatar only in avatar selection step 2015-07-29 16:30:32 -07:00
Girish Ramakrishnan e7bf8452ab randomize default avatar 2015-07-29 16:11:37 -07:00
Girish Ramakrishnan 157f972b20 Decrease size of image preview 2015-07-29 16:11:20 -07:00
Girish Ramakrishnan b36028dc11 Pick -> Choose 2015-07-29 15:55:41 -07:00
Girish Ramakrishnan 70092ec559 Ensure image got loaded before setting the preview 2015-07-29 15:53:58 -07:00
Girish Ramakrishnan 56d740d597 Merge welcome step and step2 2015-07-29 15:11:34 -07:00
Girish Ramakrishnan ed55e52363 Actually remove dtrace dep
Use --no-optional when installing dtrace
2015-07-29 10:15:25 -07:00
Johannes Zellner 89c36ae6a9 Do not show the update page if update failed 2015-07-29 14:19:15 +02:00
Johannes Zellner 3027c119fe Use angular in update dialog and show errors 2015-07-29 14:02:31 +02:00
Johannes Zellner 4f129102a8 Use -1 for progress to indicate an error 2015-07-29 13:53:36 +02:00
Johannes Zellner 2dd6bb0c67 Rename upgradeError to updateError in update 2015-07-29 13:52:59 +02:00
Johannes Zellner b928b08a4c Reset update progress on update failure 2015-07-29 12:41:19 +02:00
Johannes Zellner 9dcc6e68a4 Use new avatar set
Fixes #456
2015-07-29 11:13:59 +02:00
Girish Ramakrishnan 452e67be54 This is probably obvious 2015-07-28 23:12:53 -07:00
Girish Ramakrishnan 9e0611f6d8 Improve wording of wizard 2015-07-28 23:09:06 -07:00
Girish Ramakrishnan ad3392ef2e model is queried from appstore 2015-07-28 17:08:32 -07:00
Girish Ramakrishnan 71e8abf081 define adminOrigin in splashpage.sh 2015-07-28 16:52:27 -07:00
Girish Ramakrishnan 46172e76c6 Keep updater arguments sorted for readability 2015-07-28 16:03:32 -07:00
Girish Ramakrishnan 7e639bd0e2 Release update/upgrade lock only on error 2015-07-28 15:28:10 -07:00
Girish Ramakrishnan 7a9af5373b Check percent value before redirecting to update.html 2015-07-28 14:43:49 -07:00
Girish Ramakrishnan 3ea7a11d97 Set progress completion error messages 2015-07-28 14:40:22 -07:00
Girish Ramakrishnan f582ba1ba7 console.error any backup error message for now 2015-07-28 14:30:40 -07:00
Girish Ramakrishnan b96fc2bc56 initialize percent 2015-07-28 14:28:53 -07:00
Girish Ramakrishnan 48c16277f0 Create error object properly 2015-07-28 14:22:34 -07:00
Girish Ramakrishnan 4ad4ff0b10 Use progress.set in upgrade/update code paths 2015-07-28 14:22:08 -07:00
Girish Ramakrishnan 25f05e5abd Add missing ; 2015-07-28 13:09:24 -07:00
Girish Ramakrishnan 7c214a9181 log update and upgrade errors 2015-07-28 10:03:52 -07:00
Johannes Zellner d66b1eef59 Better support for active directory clients 2015-07-28 18:39:16 +02:00
Girish Ramakrishnan 58f52b90f8 better debug on what is being autoupdated 2015-07-28 09:37:46 -07:00
Girish Ramakrishnan edb67db4ea Remove unnecessary debug making logs very verbose 2015-07-28 09:32:19 -07:00
Johannes Zellner 733014d8d9 No need to guess the apiOrigin anymore, we redirect now
Fixes #436
2015-07-28 14:03:48 +02:00
Johannes Zellner 4980f79688 Show link to referrer in appstatus 2015-07-28 14:01:51 +02:00
Johannes Zellner 3d8b90f5c8 Redirect on app error to webadmin appstatus page
Part of #436
2015-07-28 13:46:58 +02:00
Johannes Zellner eea547411b Show testing badges in appstore view 2015-07-28 13:21:23 +02:00
Johannes Zellner af682e5bb1 Fix the app icons in the install app grid 2015-07-28 13:06:55 +02:00
Johannes Zellner 739dcfde8b Show version and author in install dialog 2015-07-28 12:53:33 +02:00
Johannes Zellner 1db58dd78d Support ?version in direct appstore URLs
Fixes #454
2015-07-28 11:49:04 +02:00
Johannes Zellner 947137b3f9 Ensure we have a fallback avatar 2015-07-28 11:28:06 +02:00
Johannes Zellner 509e2caa83 Also show avatar in nakeddomain error page 2015-07-28 11:19:13 +02:00
Johannes Zellner a0e67daa52 Use avatar in error page 2015-07-28 11:18:55 +02:00
Johannes Zellner 32584f3a90 Fix long lasting navbar padding issue 2015-07-28 10:57:48 +02:00
Johannes Zellner 3513f321fb Reload webadmin in case the avatar changes
Fixes #452
2015-07-28 10:50:33 +02:00
Johannes Zellner 8aaccbba55 Show avatar in navbar 2015-07-28 10:49:56 +02:00
Johannes Zellner 31ab86a97f Show avatar as favicon 2015-07-28 10:40:10 +02:00
Girish Ramakrishnan 2c0786eb37 Use ldapjs from github directly
The 0.7.x ldapjs is over a year old and uses dtrace as a dep which
causes issues when rebuilding.
2015-07-27 13:06:30 -07:00
Johannes Zellner 3db8ebf97f Ensure the appstore ui can operate always on manifest.tags 2015-07-27 19:29:25 +02:00
Johannes Zellner 804105ce2b Add testing section in appstore and mark testing apps
This is not some final design to indicate which app is in testing
but the logistics are there, mainly css from now

Fixes #451
2015-07-27 17:09:59 +02:00
Johannes Zellner c4bb56dc95 Show non published apps in webadmin 2015-07-27 16:34:37 +02:00
Johannes Zellner 87c76a3eb3 Read apps from actual response body 2015-07-27 16:27:50 +02:00
Johannes Zellner 6bceff14ec Add proxy api to get non approved app listings 2015-07-27 14:00:44 +02:00
Girish Ramakrishnan 6b62561706 Add mandatory addons object 2015-07-24 06:59:34 -07:00
Girish Ramakrishnan d558c06803 Add missing semicolon 2015-07-24 06:53:07 -07:00
Girish Ramakrishnan ef9508ccc5 Use BOX_ENV instead of NODE_ENV
Let NODE_ENV be used by node modules and always be set to production

Fixes #453
2015-07-24 01:42:28 -07:00
Girish Ramakrishnan ec8342c2ce Better progress messages 2015-07-23 22:50:58 -07:00
122 changed files with 2517 additions and 932 deletions
+1 -5
View File
@@ -4,10 +4,6 @@ docs/
webadmin/dist/ webadmin/dist/
setup/splash/website/ setup/splash/website/
# vim swam files # vim swap files
*.swp *.swp
# supervisor
supervisord.pid
supervisord.log
-1
View File
@@ -4,7 +4,6 @@ The Box
Development setup Development setup
----------------- -----------------
* sudo useradd -m yellowtent * sudo useradd -m yellowtent
** This dummy user is required for supervisor 'box' configs
** Add admin-localhost as 127.0.0.1 in /etc/hosts ** Add admin-localhost as 127.0.0.1 in /etc/hosts
** All apps will be installed as hypened-subdomains of localhost. You should add ** All apps will be installed as hypened-subdomains of localhost. You should add
hyphened-subdomains of your apps into /etc/hosts hyphened-subdomains of your apps into /etc/hosts
Regular → Executable
+28 -59
View File
@@ -2,77 +2,46 @@
'use strict'; 'use strict';
// WARNING This is a supervisor eventlistener! var assert = require('assert'),
// The communication happens via stdin/stdout mailer = require('./src/mailer.js'),
// !! No console.log() allowed safe = require('safetydance'),
// !! Do not set DEBUG path = require('path'),
util = require('util');
var supervisor = require('supervisord-eventlistener'), var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
assert = require('assert'),
exec = require('child_process').exec,
util = require('util'),
fs = require('fs'),
mailer = require('./src/mailer.js');
var gLastNotifyTime = {};
var gCooldownTime = 1000 * 60 * 5; // 5 min
function collectLogs(program, callback) { function collectLogs(program, callback) {
assert.strictEqual(typeof program, 'string'); assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var logFilePath = util.format('/var/log/supervisor/%s.log', program); var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
if (!fs.existsSync(logFilePath)) return callback(new Error(util.format('Log file %s does not exist.', logFilePath)));
fs.readFile(logFilePath, 'utf-8', function (error, data) {
if (error) return callback(error);
var lines = data.split('\n');
var boxLogLines = lines.slice(-100);
exec('dmesg', function (error, stdout /*, stderr */) {
if (error) console.error(error);
var lines = stdout.split('\n');
var dmesgLogLines = lines.slice(-100);
var result = '';
result += program + '.log\n';
result += '-------------------------------------\n';
result += boxLogLines.join('\n');
result += '\n\n';
result += 'dmesg\n';
result += '-------------------------------------\n';
result += dmesgLogLines.join('\n');
callback(null, result);
});
});
} }
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) { function sendCrashNotification(processName) {
if (data.expected === '1') return console.error('Normal app %s exit', data.processname); collectLogs(processName, function (error, result) {
console.error('%s exited unexpectedly', data.processname);
collectLogs(data.processname, function (error, result) {
if (error) { if (error) {
console.error('Failed to collect logs.', error); console.error('Failed to collect logs.', error);
result = util.format('Failed to collect logs.', error); result = util.format('Failed to collect logs.', error);
} }
if (!gLastNotifyTime[data.processname] || gLastNotifyTime[data.processname] < Date.now() - gCooldownTime) { console.log('Sending crash notification email for', processName);
console.error('Send mail.'); mailer.sendCrashNotification(processName, result);
mailer.sendCrashNotification(data.processname, result);
gLastNotifyTime[data.processname] = Date.now();
} else {
console.error('Do not send mail, already sent one recently.');
}
}); });
}); }
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
mailer.initialize(function (error) {
if (error) return console.error(error);
sendCrashNotification(processName);
});
}
main();
mailer.initialize(function () {
supervisor.listen(process.stdin, process.stdout);
console.error('Crashnotifier listening...');
});
+66 -78
View File
@@ -7,6 +7,28 @@
"from": "https://registry.npmjs.org/async/-/async-1.2.1.tgz", "from": "https://registry.npmjs.org/async/-/async-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz" "resolved": "https://registry.npmjs.org/async/-/async-1.2.1.tgz"
}, },
"aws-sdk": {
"version": "2.1.46",
"from": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1.46.tgz",
"dependencies": {
"sax": {
"version": "0.5.3",
"from": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz",
"resolved": "http://registry.npmjs.org/sax/-/sax-0.5.3.tgz"
},
"xml2js": {
"version": "0.2.8",
"from": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz"
},
"xmlbuilder": {
"version": "0.4.2",
"from": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.2.tgz"
}
}
},
"body-parser": { "body-parser": {
"version": "1.13.1", "version": "1.13.1",
"from": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.1.tgz", "from": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.1.tgz",
@@ -105,23 +127,24 @@
} }
}, },
"cloudron-manifestformat": { "cloudron-manifestformat": {
"version": "1.4.0", "version": "1.7.0",
"from": "cloudron-manifestformat@1.4.0", "from": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
"dependencies": { "dependencies": {
"java-packagename-regex": { "java-packagename-regex": {
"version": "1.0.0", "version": "1.0.0",
"from": "java-packagename-regex@>=1.0.0 <2.0.0", "from": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz" "resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz"
}, },
"safetydance": { "safetydance": {
"version": "0.0.15", "version": "0.0.15",
"from": "safetydance@0.0.15", "from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz" "resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz"
}, },
"tv4": { "tv4": {
"version": "1.1.12", "version": "1.2.3",
"from": "tv4@>=1.1.9 <2.0.0", "from": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz",
"resolved": "http://registry.npmjs.org/tv4/-/tv4-1.1.12.tgz" "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz"
} }
} }
}, },
@@ -131,18 +154,17 @@
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz" "resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz"
}, },
"connect-lastmile": { "connect-lastmile": {
"version": "0.0.12", "version": "0.0.13",
"from": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz", "from": "connect-lastmile@0.0.13",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.12.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "2.1.3", "version": "2.1.3",
"from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "from": "debug@>=2.1.0 <2.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
"dependencies": { "dependencies": {
"ms": { "ms": {
"version": "0.7.0", "version": "0.7.0",
"from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz", "from": "ms@0.7.0",
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz" "resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
} }
} }
@@ -1288,69 +1310,69 @@
}, },
"dockerode": { "dockerode": {
"version": "2.2.2", "version": "2.2.2",
"from": "dockerode@2.2.2", "from": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz", "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.2.2.tgz",
"dependencies": { "dependencies": {
"docker-modem": { "docker-modem": {
"version": "0.2.6", "version": "0.2.6",
"from": "docker-modem@>=0.2.0 <0.3.0", "from": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-0.2.6.tgz",
"dependencies": { "dependencies": {
"debug": { "debug": {
"version": "0.7.4", "version": "0.7.4",
"from": "debug@>=0.7.4 <0.8.0", "from": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz"
}, },
"follow-redirects": { "follow-redirects": {
"version": "0.0.3", "version": "0.0.3",
"from": "follow-redirects@0.0.3", "from": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz" "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.3.tgz"
}, },
"JSONStream": { "JSONStream": {
"version": "0.10.0", "version": "0.10.0",
"from": "JSONStream@0.10.0", "from": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"dependencies": { "dependencies": {
"jsonparse": { "jsonparse": {
"version": "0.0.5", "version": "0.0.5",
"from": "jsonparse@0.0.5", "from": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz" "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz"
}, },
"through": { "through": {
"version": "2.3.8", "version": "2.3.8",
"from": "through@>=2.2.7 <3.0.0", "from": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
} }
} }
}, },
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
"from": "querystring@0.2.0", "from": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz"
}, },
"readable-stream": { "readable-stream": {
"version": "1.0.33", "version": "1.0.33",
"from": "readable-stream@>=1.0.26-4 <1.1.0", "from": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.33.tgz",
"dependencies": { "dependencies": {
"core-util-is": { "core-util-is": {
"version": "1.0.1", "version": "1.0.1",
"from": "core-util-is@>=1.0.0 <1.1.0", "from": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz",
"resolved": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz" "resolved": "http://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
}, },
"isarray": { "isarray": {
"version": "0.0.1", "version": "0.0.1",
"from": "isarray@0.0.1", "from": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"resolved": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" "resolved": "http://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
}, },
"string_decoder": { "string_decoder": {
"version": "0.10.31", "version": "0.10.31",
"from": "string_decoder@>=0.10.0 <0.11.0", "from": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
}, },
"inherits": { "inherits": {
"version": "2.0.1", "version": "2.0.1",
"from": "inherits@>=2.0.1 <2.1.0", "from": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" "resolved": "http://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
} }
} }
@@ -1691,88 +1713,81 @@
}, },
"ldapjs": { "ldapjs": {
"version": "0.7.1", "version": "0.7.1",
"from": "ldapjs@*", "from": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz", "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-0.7.1.tgz",
"dependencies": { "dependencies": {
"asn1": { "asn1": {
"version": "0.2.1", "version": "0.2.1",
"from": "asn1@0.2.1", "from": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz" "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.1.tgz"
}, },
"assert-plus": { "assert-plus": {
"version": "0.1.5", "version": "0.1.5",
"from": "assert-plus@0.1.5", "from": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz" "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz"
}, },
"bunyan": { "bunyan": {
"version": "0.22.1", "version": "0.22.1",
"from": "bunyan@0.22.1", "from": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
"dependencies": {
"mv": {
"version": "0.0.5",
"from": "mv@0.0.5",
"resolved": "https://registry.npmjs.org/mv/-/mv-0.0.5.tgz"
}
}
}, },
"nopt": { "nopt": {
"version": "2.1.1", "version": "2.1.1",
"from": "nopt@2.1.1", "from": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nopt/-/nopt-2.1.1.tgz",
"dependencies": { "dependencies": {
"abbrev": { "abbrev": {
"version": "1.0.7", "version": "1.0.7",
"from": "abbrev@>=1.0.0 <2.0.0", "from": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz" "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.7.tgz"
} }
} }
}, },
"pooling": { "pooling": {
"version": "0.4.6", "version": "0.4.6",
"from": "pooling@0.4.6", "from": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz",
"resolved": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz", "resolved": "https://registry.npmjs.org/pooling/-/pooling-0.4.6.tgz",
"dependencies": { "dependencies": {
"once": { "once": {
"version": "1.3.0", "version": "1.3.0",
"from": "once@1.3.0", "from": "https://registry.npmjs.org/once/-/once-1.3.0.tgz",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz" "resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz"
}, },
"vasync": { "vasync": {
"version": "1.4.0", "version": "1.4.0",
"from": "vasync@1.4.0", "from": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz", "resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.0.tgz",
"dependencies": { "dependencies": {
"jsprim": { "jsprim": {
"version": "0.3.0", "version": "0.3.0",
"from": "jsprim@0.3.0", "from": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
"dependencies": { "dependencies": {
"extsprintf": { "extsprintf": {
"version": "1.0.0", "version": "1.0.0",
"from": "extsprintf@1.0.0", "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz" "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz"
}, },
"json-schema": { "json-schema": {
"version": "0.2.2", "version": "0.2.2",
"from": "json-schema@0.2.2", "from": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz"
}, },
"verror": { "verror": {
"version": "1.3.3", "version": "1.3.3",
"from": "verror@1.3.3", "from": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz" "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz"
} }
} }
}, },
"verror": { "verror": {
"version": "1.1.0", "version": "1.1.0",
"from": "verror@1.1.0", "from": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
"dependencies": { "dependencies": {
"extsprintf": { "extsprintf": {
"version": "1.0.0", "version": "1.0.0",
"from": "extsprintf@1.0.0", "from": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz" "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz"
} }
} }
@@ -1780,11 +1795,6 @@
} }
} }
} }
},
"dtrace-provider": {
"version": "0.2.8",
"from": "dtrace-provider@0.2.8",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz"
} }
} }
}, },
@@ -1950,23 +1960,6 @@
"from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz", "from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz" "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz"
}, },
"nodejs-disks": {
"version": "0.2.1",
"from": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
"resolved": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
"dependencies": {
"async": {
"version": "0.2.10",
"from": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
},
"numeral": {
"version": "1.4.8",
"from": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz",
"resolved": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz"
}
}
},
"nodemailer": { "nodemailer": {
"version": "1.3.4", "version": "1.3.4",
"from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.3.4.tgz", "from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.3.4.tgz",
@@ -2273,9 +2266,9 @@
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.0.tgz" "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.13.0.tgz"
}, },
"safetydance": { "safetydance": {
"version": "0.0.16", "version": "0.0.19",
"from": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz", "from": "safetydance@0.0.19",
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.16.tgz" "resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
}, },
"semver": { "semver": {
"version": "4.3.6", "version": "4.3.6",
@@ -2448,11 +2441,6 @@
} }
} }
}, },
"supervisord-eventlistener": {
"version": "0.1.0",
"from": "https://registry.npmjs.org/supervisord-eventlistener/-/supervisord-eventlistener-0.1.0.tgz",
"resolved": "https://registry.npmjs.org/supervisord-eventlistener/-/supervisord-eventlistener-0.1.0.tgz"
},
"tail-stream": { "tail-stream": {
"version": "0.2.1", "version": "0.2.1",
"from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz", "from": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
+7 -9
View File
@@ -17,10 +17,11 @@
}, },
"dependencies": { "dependencies": {
"async": "^1.2.1", "async": "^1.2.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1", "body-parser": "^1.13.1",
"cloudron-manifestformat": "^1.4.0", "cloudron-manifestformat": "^1.7.0",
"connect-ensure-login": "^0.1.1", "connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.12", "connect-lastmile": "0.0.13",
"connect-timeout": "^1.5.0", "connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5", "cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0", "cookie-session": "^1.1.0",
@@ -43,7 +44,6 @@
"mysql": "^2.7.0", "mysql": "^2.7.0",
"native-dns": "^0.7.0", "native-dns": "^0.7.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"nodejs-disks": "^0.2.1",
"nodemailer": "^1.3.0", "nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3", "nodemailer-smtp-transport": "^1.0.3",
"oauth2orize": "^1.0.1", "oauth2orize": "^1.0.1",
@@ -55,13 +55,12 @@
"passport-oauth2-client-password": "^0.1.2", "passport-oauth2-client-password": "^0.1.2",
"password-generator": "^1.0.0", "password-generator": "^1.0.0",
"proxy-middleware": "^0.13.0", "proxy-middleware": "^0.13.0",
"safetydance": "0.0.16", "safetydance": "0.0.19",
"semver": "^4.3.6", "semver": "^4.3.6",
"serve-favicon": "^2.2.0", "serve-favicon": "^2.2.0",
"split": "^1.0.0", "split": "^1.0.0",
"superagent": "~0.21.0", "superagent": "~0.21.0",
"supererror": "^0.7.0", "supererror": "^0.7.0",
"supervisord-eventlistener": "^0.1.0",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz", "tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"underscore": "^1.7.0", "underscore": "^1.7.0",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
@@ -69,7 +68,6 @@
}, },
"devDependencies": { "devDependencies": {
"apidoc": "*", "apidoc": "*",
"aws-sdk": "^2.1.10",
"bootstrap-sass": "^3.3.3", "bootstrap-sass": "^3.3.3",
"del": "^1.1.1", "del": "^1.1.1",
"expect.js": "*", "expect.js": "*",
@@ -92,9 +90,9 @@
"yargs": "^3.15.0" "yargs": "^3.15.0"
}, },
"scripts": { "scripts": {
"migrate_local": "NODE_ENV=local DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up", "migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "NODE_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up", "migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test", "test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true", "postmerge": "/bin/true",
"precommit": "/bin/true", "precommit": "/bin/true",
"prepush": "npm test", "prepush": "npm test",
+2 -2
View File
@@ -16,7 +16,7 @@ and replace it with a new one for an update.
Because we do not package things as Docker yet, we should be careful Because we do not package things as Docker yet, we should be careful
about the code here. We have to expect remains of an older setup code. about the code here. We have to expect remains of an older setup code.
For example, older supervisor or nginx configs might be around. For example, older systemd or nginx configs might be around.
The config directory is _part_ of the container and is not a VOLUME. The config directory is _part_ of the container and is not a VOLUME.
Which is to say that the files will be nuked from one update to the next. Which is to say that the files will be nuked from one update to the next.
@@ -40,7 +40,7 @@ version (see below) or the mysql/postgresql data etc.
* It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf. * It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf.
* supervisor is then started * box services are then started
setup_infra.sh setup_infra.sh
This setups containers like graphite, mail and the addons containers. This setups containers like graphite, mail and the addons containers.
+12 -1
View File
@@ -3,4 +3,15 @@
# If you change the infra version, be sure to put a warning # If you change the infra version, be sure to put a warning
# in the change log # in the change log
INFRA_VERSION=4 INFRA_VERSION=8
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.3.3
MYSQL_IMAGE=cloudron/mysql:0.3.3
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
MONGODB_IMAGE=cloudron/mongodb:0.3.2
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.3.2
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
+21 -17
View File
@@ -3,19 +3,21 @@
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${script_dir}/../node_modules/.bin/json" json="${script_dir}/../node_modules/.bin/json"
arg_restore_url="" # IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_restore_key="" arg_api_server_origin=""
arg_box_versions_url="" arg_box_versions_url=""
arg_fqdn=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire="false"
arg_tls_cert="" arg_tls_cert=""
arg_tls_key="" arg_tls_key=""
arg_api_server_origin=""
arg_web_server_origin=""
arg_fqdn=""
arg_token="" arg_token=""
arg_version="" arg_version=""
arg_is_custom_domain="false" arg_web_server_origin=""
arg_retire="false" arg_backup_key=""
arg_model="" arg_aws=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@") args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}" eval set -- "${args}"
@@ -41,8 +43,11 @@ EOF
arg_restore_key=$(echo "$2" | $json restoreKey) arg_restore_key=$(echo "$2" | $json restoreKey)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key="" [[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_model=$(echo "$2" | $json model) arg_backup_key=$(echo "$2" | $json backupKey)
[[ "${arg_model}" == "null" ]] && arg_model="" [[ "${arg_backup_key}" == "null" ]] && arg_backup_key=""
arg_aws=$(echo "$2" | $json aws)
[[ "${arg_aws}" == "null" ]] && arg_aws=""
shift 2 shift 2
;; ;;
@@ -52,15 +57,14 @@ EOF
done done
echo "Parsed arguments:" echo "Parsed arguments:"
echo "restore url: ${arg_restore_url}"
echo "restore key: ${arg_restore_key}"
echo "box versions url: ${arg_box_versions_url}"
echo "api server: ${arg_api_server_origin}" echo "api server: ${arg_api_server_origin}"
echo "web server: ${arg_web_server_origin}" echo "box versions url: ${arg_box_versions_url}"
echo "fqdn: ${arg_fqdn}" echo "fqdn: ${arg_fqdn}"
echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "custom domain: ${arg_is_custom_domain}" echo "custom domain: ${arg_is_custom_domain}"
echo "restore key: ${arg_restore_key}"
echo "restore url: ${arg_restore_url}"
echo "tls cert: ${arg_tls_cert}" echo "tls cert: ${arg_tls_cert}"
echo "tls key: ${arg_tls_key}" echo "tls key: ${arg_tls_key}"
echo "model: ${arg_model}" echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
+7 -7
View File
@@ -13,13 +13,10 @@ readonly DATA_DIR="/home/yellowtent/data"
rm -rf "${CONFIG_DIR}" rm -rf "${CONFIG_DIR}"
sudo -u yellowtent mkdir "${CONFIG_DIR}" sudo -u yellowtent mkdir "${CONFIG_DIR}"
########## logrotate (default ubuntu runs this daily) ########## systemd
rm -rf /etc/logrotate.d/* cp -r "${container_files}/systemd/." /etc/systemd/system/
cp -r "${container_files}/logrotate/." /etc/logrotate.d/ systemctl daemon-reload
systemctl enable cloudron.target
########## supervisor
rm -rf /etc/supervisor/*
cp -r "${container_files}/supervisor/." /etc/supervisor/
########## sudoers ########## sudoers
rm /etc/sudoers.d/* rm /etc/sudoers.d/*
@@ -34,6 +31,9 @@ ln -sfF "${DATA_DIR}/collectd" /etc/collectd
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
ln -s "${DATA_DIR}/nginx" /etc/nginx ln -s "${DATA_DIR}/nginx" /etc/nginx
########## mysql
cp "${container_files}/mysql.cnf" /etc/mysql/mysql.cnf
########## Enable services ########## Enable services
update-rc.d -f collectd defaults update-rc.d -f collectd defaults
-6
View File
@@ -1,6 +0,0 @@
/var/log/cloudron/*log {
missingok
notifempty
size 100k
nocompress
}
-7
View File
@@ -1,7 +0,0 @@
/var/log/supervisor/*log {
missingok
copytruncate
notifempty
size 100k
nocompress
}
+7
View File
@@ -0,0 +1,7 @@
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connection=50
+12 -9
View File
@@ -1,26 +1,29 @@
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
Defaults!/home/yellowtent/box/src/scripts/backupbox.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/backupbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupbox.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupbox.sh
Defaults!/home/yellowtent/box/src/scripts/backupapp.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/backupapp.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupapp.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupapp.sh
Defaults!/home/yellowtent/box/src/scripts/restoreapp.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/restoreapp.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restoreapp.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restoreapp.sh
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME NODE_ENV" Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
@@ -1,10 +0,0 @@
[program:apphealthtask]
command=/usr/bin/node "/home/yellowtent/box/apphealthtask.js"
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/apphealthtask.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
@@ -1,10 +0,0 @@
[program:box]
command=/usr/bin/node "/home/yellowtent/box/app.js"
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/box.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*,connect-lastmile",NODE_ENV="cloudron"
@@ -1,11 +0,0 @@
[eventlistener:crashnotifier]
command=/usr/bin/node "/home/yellowtent/box/crashnotifier.js"
events=PROCESS_STATE
autostart=true
autorestart=true
redirect_stderr=false
stderr_logfile=/var/log/supervisor/crashnotifier.log
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",NODE_ENV="cloudron"
@@ -1,10 +0,0 @@
[program:janitor]
command=/usr/bin/node "/home/yellowtent/box/janitor.js"
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/janitor.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
@@ -1,10 +0,0 @@
[program:oauthproxy]
command=/usr/bin/node "/home/yellowtent/box/oauthproxy.js"
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/supervisor/oauthproxy.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=2
user=yellowtent
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",NODE_ENV="cloudron"
@@ -1,33 +0,0 @@
; supervisor config file
; http://coffeeonthekeyboard.com/using-supervisorctl-with-linux-permissions-but-without-root-or-sudo-977/
[inet_http_server]
port = 127.0.0.1:9001
[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
logfile_maxbytes = 50MB
logfile_backups=10
loglevel = info
nodaemon = false
childlogdir = /var/log/supervisor/
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=http://127.0.0.1:9001
; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.
[include]
files = conf.d/*.conf
@@ -0,0 +1,15 @@
[Unit]
Description=Cloudron App Health Monitor
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/apphealthtask.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
+17
View File
@@ -0,0 +1,17 @@
[Unit]
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/app.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=200M
TimeoutStopSec=5s
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Cloudron Smart Cloud
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=apphealthtask.service box.service janitor.service oauthproxy.service
After=apphealthtask.service box.service janitor.service oauthproxy.service
# AllowIsolate=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,15 @@
# http://northernlightlabs.se/systemd.status.mail.on.unit.failure
[Unit]
Description=Cloudron Crash Notifier for %i
# otherwise, systemd will kill this unit immediately as nobody requires it
StopWhenUnneeded=false
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
ExecStart="/home/yellowtent/box/crashnotifier.js" %I
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Cloudron Janitor
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/janitor.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
@@ -0,0 +1,16 @@
[Unit]
Description=Cloudron OAuth Proxy Service
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart="/home/yellowtent/box/oauthproxy.js"
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
Group=yellowtent
MemoryLimit=50M
+7 -6
View File
@@ -7,6 +7,7 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly BOX_SRC_DIR="/home/yellowtent/box" readonly BOX_SRC_DIR="/home/yellowtent/box"
readonly DATA_DIR="/home/yellowtent/data" readonly DATA_DIR="/home/yellowtent/data"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION
@@ -14,6 +15,10 @@ echo "Setting up nginx update page"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
# copy the website # copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}" rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}" cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
@@ -24,14 +29,10 @@ infra_version="none"
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
rm -f ${DATA_DIR}/nginx/applications/* rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf" -O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else else
# keep this is sync with config.js appFqdn()
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf" -O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json" echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
+19 -19
View File
@@ -19,6 +19,7 @@ source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used
# keep this is sync with config.js appFqdn() # keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}") admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
readonly is_update=$([[ -d "${DATA_DIR}/box" ]] && echo "true" || echo "false") readonly is_update=$([[ -d "${DATA_DIR}/box" ]] && echo "true" || echo "false")
@@ -40,6 +41,9 @@ mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/mail" mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/graphite" mkdir -p "${DATA_DIR}/graphite"
mkdir -p "${DATA_DIR}/mysql"
mkdir -p "${DATA_DIR}/postgresql"
mkdir -p "${DATA_DIR}/mongodb"
mkdir -p "${DATA_DIR}/snapshots" mkdir -p "${DATA_DIR}/snapshots"
mkdir -p "${DATA_DIR}/addons" mkdir -p "${DATA_DIR}/addons"
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d" mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
@@ -52,6 +56,9 @@ echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_version
echo "Cleaning up snapshots" echo "Cleaning up snapshots"
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
# restart mysql to make sure it has latest config
service mysql restart
readonly mysql_root_password="password" readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box' mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
@@ -77,11 +84,15 @@ set_progress "25" "Migrating data"
sudo -u "${USER}" -H bash <<EOF sudo -u "${USER}" -H bash <<EOF
set -eu set -eu
cd "${BOX_SRC_DIR}" cd "${BOX_SRC_DIR}"
NODE_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF EOF
set_progress "28" "Setup collectd" set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf" cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
vda1_id=$(blkid -s UUID -o value /dev/vda1)
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
service collectd restart service collectd restart
set_progress "30" "Setup nginx" set_progress "30" "Setup nginx"
@@ -95,7 +106,7 @@ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs
# generate these for update code paths as well to overwrite splash # generate these for update code paths as well to overwrite splash
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \ ${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf" -O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert" mkdir -p "${DATA_DIR}/nginx/cert"
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
@@ -108,10 +119,10 @@ set_progress "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}" ${script_dir}/start/setup_infra.sh "${arg_fqdn}"
set_progress "65" "Creating cloudron.conf" set_progress "65" "Creating cloudron.conf"
admin_origin="https://${admin_fqdn}"
sudo -u yellowtent -H bash <<EOF sudo -u yellowtent -H bash <<EOF
set -eu set -eu
echo "Creating cloudron.conf" echo "Creating cloudron.conf"
# note that arg_aws is a javascript object and intentionally unquoted below
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{ {
"version": "${arg_version}", "version": "${arg_version}",
@@ -129,7 +140,8 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"port": 3306, "port": 3306,
"name": "box" "name": "box"
}, },
"model": "${arg_model}" "backupKey": "${arg_backup_key}",
"aws": ${arg_aws}
} }
CONF_END CONF_END
@@ -154,22 +166,10 @@ ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
mysql -u root -p${mysql_root_password} \ mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box -e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
set_progress "80" "Reloading supervisor" set_progress "80" "Starting Cloudron"
# looks like restarting supervisor completely is the only way to reload it systemctl start cloudron.target
service supervisor stop || true
echo -n "Waiting for supervisord to stop" sleep 2 # give systemd sometime to start the processes
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
echo -n "."
sleep 1
done
echo ""
echo "Starting supervisor"
service supervisor start
sleep 2 # give supervisor sometime to start the processes
set_progress "85" "Reloading nginx" set_progress "85" "Reloading nginx"
nginx -s reload nginx -s reload
+3 -4
View File
@@ -193,12 +193,11 @@ LoadPlugin write_graphite
</Plugin> </Plugin>
<Plugin df> <Plugin df>
Device "/dev/vda1" FSType "tmpfs"
Device "/dev/loop0" MountPoint "/dev"
Device "/dev/loop1"
ReportByDevice true ReportByDevice true
IgnoreSelected false IgnoreSelected true
ValuesAbsolute true ValuesAbsolute true
ValuesPercentage true ValuesPercentage true
+2 -4
View File
@@ -37,11 +37,9 @@ server {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
error_page 500 502 503 504 =200 @appstatus; error_page 500 502 503 504 @appstatus;
location @appstatus { location @appstatus {
internal; return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
root <%= sourceDir %>/webadmin/dist;
rewrite ^/$ /appstatus.html break;
} }
location / { location / {
+11 -9
View File
@@ -27,11 +27,13 @@ if [[ -n "${existing_containers}" ]]; then
fi fi
# graphite # graphite
docker run --restart=always -d --name="graphite" \ graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-p 127.0.0.1:2003:2003 \ -p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \ -p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \ -p 127.0.0.1:8000:8000 \
-v "${DATA_DIR}/graphite:/app/data" cloudron/graphite:0.3.1 -v "${DATA_DIR}/graphite:/app/data" \
"${GRAPHITE_IMAGE}")
echo "Graphite container id: ${graphite_container_id}"
# mail # mail
mail_container_id=$(docker run --restart=always -d --name="mail" \ mail_container_id=$(docker run --restart=always -d --name="mail" \
@@ -39,7 +41,7 @@ mail_container_id=$(docker run --restart=always -d --name="mail" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \ -e "DOMAIN_NAME=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \ -v "${DATA_DIR}/box/mail:/app/data" \
cloudron/mail:0.3.0) "${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}" echo "Mail container id: ${mail_container_id}"
# mysql # mysql
@@ -52,8 +54,8 @@ EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \ mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \ -v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:r" \ -v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
cloudron/mysql:0.3.0) "${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}" echo "MySQL container id: ${mysql_container_id}"
# postgresql # postgresql
@@ -64,8 +66,8 @@ EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \ postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \ -v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:r" \ -v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
cloudron/postgresql:0.3.0) "${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}" echo "PostgreSQL container id: ${postgresql_container_id}"
# mongodb # mongodb
@@ -76,8 +78,8 @@ EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \ mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-h "${arg_fqdn}" \ -h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \ -v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:r" \ -v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
cloudron/mongodb:0.3.0) "${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}" echo "Mongodb container id: ${mongodb_container_id}"
if [[ "${infra_version}" == "none" ]]; then if [[ "${infra_version}" == "none" ]]; then
+2 -10
View File
@@ -2,14 +2,6 @@
set -eu -o pipefail set -eu -o pipefail
echo "Stopping box code" echo "Stopping cloudron"
service supervisor stop || true
echo -n "Waiting for supervisord to stop"
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
echo -n "."
sleep 1
done
echo ""
systemctl stop cloudron.target
+3 -4
View File
@@ -38,8 +38,7 @@ var appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'), tokendb = require('./tokendb.js'),
util = require('util'), util = require('util'),
uuid = require('node-uuid'), uuid = require('node-uuid'),
vbox = require('./vbox.js'), vbox = require('./vbox.js');
_ = require('underscore');
var NOOP = function (app, callback) { return callback(); }; var NOOP = function (app, callback) { return callback(); };
@@ -665,7 +664,7 @@ function setupRedis(app, callback) {
name: 'redis-' + app.id, name: 'redis-' + app.id,
Hostname: config.appFqdn(app.location), Hostname: config.appFqdn(app.location),
Tty: true, Tty: true,
Image: 'cloudron/redis:0.3.0', Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null, Cmd: null,
Volumes: {}, Volumes: {},
VolumesFrom: [] VolumesFrom: []
@@ -675,7 +674,7 @@ function setupRedis(app, callback) {
var startOptions = { var startOptions = {
Binds: [ Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:r', redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw' redisDataDir + ':/var/lib/redis:rw'
], ],
// On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work // On Mac (boot2docker), we have to export the port to external world for port forwarding from Mac to work
+3 -1
View File
@@ -35,12 +35,13 @@ exports = module.exports = {
ISTATE_ERROR: 'error', // error executing last pending_* command ISTATE_ERROR: 'error', // error executing last pending_* command
ISTATE_INSTALLED: 'installed', // app is installed ISTATE_INSTALLED: 'installed', // app is installed
// run codes (keep in sync in UI)
RSTATE_RUNNING: 'running', RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start', RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop', RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by use RSTATE_STOPPED: 'stopped', // app stopped by use
RSTATE_ERROR: 'error',
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy', HEALTH_HEALTHY: 'healthy',
HEALTH_UNHEALTHY: 'unhealthy', HEALTH_UNHEALTHY: 'unhealthy',
HEALTH_ERROR: 'error', HEALTH_ERROR: 'error',
@@ -335,6 +336,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
// Rules are: // Rules are:
// uninstall is allowed in any state // uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state // restore is allowed from installed or error state
// update and configure are allowed only in installed state // update and configure are allowed only in installed state
+32 -28
View File
@@ -490,28 +490,30 @@ function restore(appId, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var restoreConfig = app.lastBackupConfig; // restore without a backup is the same as re-install
if (!restoreConfig) return callback(new AppsError(AppsError.BAD_STATE, 'No restore point')); var restoreConfig = app.lastBackupConfig, values = { };
if (restoreConfig) {
// re-validate because this new box version may not accept old configs.
// if we restore location, it should be validated here as well
error = checkManifestConstraints(restoreConfig.manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
// re-validate because this new box version may not accept old configs. if we restore location, it should be validated here as well error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
error = checkManifestConstraints(restoreConfig.manifest); if (error) return callback(error);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now // ## should probably query new location, access restriction from user
if (error) return callback(error); values = {
manifest: restoreConfig.manifest,
portBindings: restoreConfig.portBindings,
// ## should probably query new location, access restriction from user oldConfig: {
var values = { location: app.location,
manifest: restoreConfig.manifest, accessRestriction: app.accessRestriction,
portBindings: restoreConfig.portBindings, portBindings: app.portBindings,
manifest: app.manifest
oldConfig: { }
location: app.location, };
accessRestriction: app.accessRestriction, }
portBindings: app.portBindings,
manifest: app.manifest
}
};
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) { appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
@@ -573,6 +575,8 @@ function stop(appId, callback) {
} }
function checkManifestConstraints(manifest) { function checkManifestConstraints(manifest) {
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) { if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
return new Error('Box version exceeds Apps maxBoxVersion'); return new Error('Box version exceeds Apps maxBoxVersion');
} }
@@ -664,7 +668,7 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
return iteratorDone(); return iteratorDone();
} }
update(appId, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) { update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
if (error) debug('Error initiating autoupdate of %s', appId); if (error) debug('Error initiating autoupdate of %s', appId);
iteratorDone(null); iteratorDone(null);
@@ -675,16 +679,16 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
function backupApp(app, addonsToBackup, callback) { function backupApp(app, addonsToBackup, callback) {
assert.strictEqual(typeof app, 'object'); assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToBackup, 'object'); assert(!addonsToBackup || typeof addonsToBackup, 'object');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
function canBackupApp(app) { function canBackupApp(app) {
// only backup apps that are installed or pending configure. Rest of them are in some // only backup apps that are installed or pending configure. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely) // state not good for consistent backup (i.e addons may not have been setup completely)
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|| app.installationState === appdb.ISTATE_PENDING_CONFIGURE app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|| app.installationState === appdb.ISTATE_PENDING_BACKUP app.installationState === appdb.ISTATE_PENDING_BACKUP ||
|| app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
} }
if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy')); if (!canBackupApp(app)) return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy'));
@@ -700,7 +704,7 @@ function backupApp(app, addonsToBackup, callback) {
return callback(safe.error); return callback(safe.error);
} }
backups.getBackupUrl(app, null, function (error, result) { backups.getBackupUrl(app, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -709,7 +713,7 @@ function backupApp(app, addonsToBackup, callback) {
async.series([ async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
addons.backupAddons.bind(null, app, addonsToBackup), addons.backupAddons.bind(null, app, addonsToBackup),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey ]), shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) { ], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -756,7 +760,7 @@ function restoreApp(app, addonsToRestore, callback) {
debugApp(app, 'restoreApp: restoreUrl:%s', result.url); debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey ], function (error) { shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
addons.restoreAddons(app, addonsToRestore, callback); addons.restoreAddons(app, addonsToRestore, callback);
+53 -72
View File
@@ -46,6 +46,7 @@ var addons = require('./addons.js'),
paths = require('./paths.js'), paths = require('./paths.js'),
safe = require('safetydance'), safe = require('safetydance'),
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
util = require('util'), util = require('util'),
@@ -93,7 +94,7 @@ function configureNginx(app, callback) {
var sourceDir = path.resolve(__dirname, '..'); var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.accessRestriction ? 'oauthproxy' : 'app'; var endpoint = app.accessRestriction ? 'oauthproxy' : 'app';
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint }); var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, { sourceDir: sourceDir, adminOrigin: config.adminOrigin(), vhost: config.appFqdn(app.location), port: freePort, endpoint: endpoint });
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf'); var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debugApp(app, 'writing config to %s', nginxConfigFilename); debugApp(app, 'writing config to %s', nginxConfigFilename);
@@ -189,7 +190,6 @@ function createContainer(app, callback) {
} }
env.push('CLOUDRON=1'); env.push('CLOUDRON=1');
env.push('ADMIN_ORIGIN' + '=' + config.adminOrigin()); // ## remove
env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin()); env.push('WEBADMIN_ORIGIN' + '=' + config.adminOrigin());
env.push('API_ORIGIN' + '=' + config.adminOrigin()); env.push('API_ORIGIN' + '=' + config.adminOrigin());
@@ -202,8 +202,6 @@ function createContainer(app, callback) {
Tty: true, Tty: true,
Image: app.manifest.dockerImage, Image: app.manifest.dockerImage,
Cmd: null, Cmd: null,
Volumes: {},
VolumesFrom: [],
Env: env.concat(addonEnv), Env: env.concat(addonEnv),
ExposedPorts: exposedPorts ExposedPorts: exposedPorts
}; };
@@ -251,7 +249,7 @@ function deleteImage(app, manifest, callback) {
noprune: false noprune: false
}; };
// delete image by id because docker pull pulls down all the tags and this is the only way to delete all tags // delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
docker.getImage(result.Id).remove(removeOptions, function (error) { docker.getImage(result.Id).remove(removeOptions, function (error) {
if (error && error.statusCode === 404) return callback(null); if (error && error.statusCode === 404) return callback(null);
if (error && error.statusCode === 409) return callback(null); // another container using the image if (error && error.statusCode === 409) return callback(null); // another container using the image
@@ -334,15 +332,20 @@ function startContainer(app, callback) {
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort); vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
} }
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
var startOptions = { var startOptions = {
Binds: addons.getBindsSync(app, app.manifest.addons), Binds: addons.getBindsSync(app, app.manifest.addons),
Memory: memoryLimit / 2,
MemorySwap: memoryLimit, // Memory + Swap
PortBindings: dockerPortBindings, PortBindings: dockerPortBindings,
PublishAllPorts: false, PublishAllPorts: false,
Links: addons.getLinksSync(app, app.manifest.addons), Links: addons.getLinksSync(app, app.manifest.addons),
RestartPolicy: { RestartPolicy: {
"Name": "always", "Name": "always",
"MaximumRetryCount": 0 "MaximumRetryCount": 0
} },
CpuShares: 512 // relative to 1024 for system processes
}; };
var container = docker.getContainer(app.containerId); var container = docker.getContainer(app.containerId);
@@ -357,6 +360,11 @@ function startContainer(app, callback) {
} }
function stopContainer(app, callback) { function stopContainer(app, callback) {
if (!app.containerId) {
debugApp(app, 'No previous container to stop');
return callback();
}
var container = docker.getContainer(app.containerId); var container = docker.getContainer(app.containerId);
debugApp(app, 'Stopping container %s', container.id); debugApp(app, 'Stopping container %s', container.id);
@@ -422,43 +430,27 @@ function registerSubdomain(app, callback) {
// need to register it so that we have a dnsRecordId to wait for it to complete // need to register it so that we have a dnsRecordId to wait for it to complete
var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() }; var record = { subdomain: app.location, type: 'A', value: sysinfo.getIp() };
superagent subdomains.add(record, function (error, changeId) {
.post(config.apiServerOrigin() + '/api/v1/subdomains') if (error) return callback(error);
.set('Accept', 'application/json')
.query({ token: config.token() })
.send({ records: [ record ] })
.end(function (error, res) {
if (error) return callback(error);
debugApp(app, 'Registered subdomain status: %s', res.status); debugApp(app, 'Registered subdomain.');
if (res.status === 409) return callback(null); // already registered updateApp(app, { dnsRecordId: changeId }, callback);
if (res.status !== 201) return callback(new Error(util.format('Subdomain Registration failed. %s %j', res.status, res.body))); });
updateApp(app, { dnsRecordId: res.body.ids[0] }, callback);
});
} }
function unregisterSubdomain(app, callback) { function unregisterSubdomain(app, location, callback) {
debugApp(app, 'Unregistering subdomain: dnsRecordId=%s', app.dnsRecordId); debugApp(app, 'Unregistering subdomain: %s', location);
if (!app.dnsRecordId) return callback(null);
// do not unregister bare domain because we show a error/cloudron info page there // do not unregister bare domain because we show a error/cloudron info page there
if (app.location === '') return updateApp(app, { dnsRecordId: null }, callback); if (location === '') return callback(null);
superagent var record = { subdomain: location, type: 'A', value: sysinfo.getIp() };
.del(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId) subdomains.remove(record, function (error) {
.query({ token: config.token() }) if (error) debugApp(app, 'Error unregistering subdomain: %s', error);
.end(function (error, res) {
if (error) {
debugApp(app, 'Error making request: %s', error);
} else if (res.status !== 204) {
debugApp(app, 'Error unregistering subdomain:', res.status, res.body);
}
updateApp(app, { dnsRecordId: null }, callback); updateApp(app, { dnsRecordId: null }, callback);
}); });
} }
function removeIcon(app, callback) { function removeIcon(app, callback) {
@@ -479,21 +471,15 @@ function waitForDnsPropagation(app, callback) {
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000); setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
} }
superagent subdomains.status(app.dnsRecordId, function (error, result) {
.get(config.apiServerOrigin() + '/api/v1/subdomains/' + app.dnsRecordId + '/status') if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
.set('Accept', 'application/json')
.query({ token: config.token() })
.end(function (error, res) {
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, res.status); debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
if (res.status !== 200) return retry(new Error(util.format('Error getting record status: %s %j', res.status, res.body))); if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
if (res.body.status !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, res.body.status))); callback(null);
});
callback(null);
});
} }
// updates the app object and the database // updates the app object and the database
@@ -532,9 +518,9 @@ function install(app, callback) {
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
addons.teardownAddons.bind(null, app, app.manifest.addons), addons.teardownAddons.bind(null, app, app.manifest.addons),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
unregisterSubdomain.bind(null, app), unregisterSubdomain.bind(null, app, app.location),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app), // removeIcon.bind(null, app), // do not remove icon for non-appstore installs
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }), updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
@@ -619,7 +605,11 @@ function restore(app, callback) {
// oldConfig can be null during upgrades // oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null), addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
deleteVolume.bind(null, app), deleteVolume.bind(null, app),
deleteImage.bind(null, app, app.manifest), function deleteImageIfChanged(done) {
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
deleteImage(app, app.oldConfig.manifest, done);
},
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app), removeIcon.bind(null, app),
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
@@ -673,16 +663,15 @@ function restore(app, callback) {
// note that configure is called after an infra update as well // note that configure is called after an infra update as well
function configure(app, callback) { function configure(app, callback) {
var locationChanged = app.oldConfig.location !== app.location;
async.series([ async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
removeCollectdProfile.bind(null, app), removeCollectdProfile.bind(null, app),
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
function (next) { function (next) {
if (!locationChanged) return next(); // oldConfig can be null during an infra update
unregisterSubdomain(app, next); if (!app.oldConfig || app.oldConfig.location === app.location) return next();
unregisterSubdomain(app, app.oldConfig.location, next);
}, },
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
unconfigureNginx.bind(null, app), unconfigureNginx.bind(null, app),
@@ -693,14 +682,8 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }), updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app), allocateOAuthProxyCredentials.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
if (!locationChanged) return next(); registerSubdomain.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app)
], next);
},
// re-setup addons since they rely on the app's fqdn (e.g oauth) // re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }), updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
@@ -714,14 +697,8 @@ function configure(app, callback) {
runApp.bind(null, app), runApp.bind(null, app),
function (next) { updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
if (!locationChanged) return next(); exports._waitForDnsPropagation.bind(null, app),
async.series([
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app)
], next);
},
// done! // done!
function (callback) { function (callback) {
@@ -755,7 +732,11 @@ function update(app, callback) {
stopApp.bind(null, app), stopApp.bind(null, app),
deleteContainer.bind(null, app), deleteContainer.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons), addons.teardownAddons.bind(null, app, unusedAddons),
deleteImage.bind(null, app, app.manifest), // delete image even if did not change (see df158b111f) function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
deleteImage(app, app.oldConfig.manifest, done);
},
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time... // removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
function (next) { function (next) {
@@ -821,7 +802,7 @@ function uninstall(app, callback) {
deleteImage.bind(null, app, app.manifest), deleteImage.bind(null, app, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }), updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app), unregisterSubdomain.bind(null, app, app.location),
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }), updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
removeOAuthProxyCredentials.bind(null, app), removeOAuthProxyCredentials.bind(null, app),
+282
View File
@@ -0,0 +1,282 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
AWSError: AWSError,
getAWSCredentials: getAWSCredentials,
getSignedUploadUrl: getSignedUploadUrl,
getSignedDownloadUrl: getSignedDownloadUrl,
addSubdomain: addSubdomain,
delSubdomain: delSubdomain,
getChangeStatus: getChangeStatus
};
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('./config.js'),
debug = require('debug')('box:aws'),
SubdomainError = require('./subdomainerror.js'),
superagent = require('superagent'),
util = require('util');
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AWSError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(AWSError, Error);
AWSError.INTERNAL_ERROR = 'Internal Error';
AWSError.MISSING_CREDENTIALS = 'Missing AWS credentials';
function getAWSCredentials(callback) {
assert.strictEqual(typeof callback, 'function');
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 201) return callback(new Error(result.text));
if (!result.body || !result.body.credentials) return callback(new Error('Unexpected response'));
return callback(null, {
accessKeyId: result.body.credentials.AccessKeyId,
secretAccessKey: result.body.credentials.SecretAccessKey,
sessionToken: result.body.credentials.SessionToken,
region: 'us-east-1'
});
});
} else {
if (!config.aws().accessKeyId || !config.aws().secretAccessKey) return callback(new AWSError(AWSError.MISSING_CREDENTIALS));
callback(null, {
accessKeyId: config.aws().accessKeyId,
secretAccessKey: config.aws().secretAccessKey,
region: 'us-east-1'
});
}
}
function getSignedUploadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedUploadUrl()');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('putObject', params);
callback(null, { url : url, sessionToken: credentials.sessionToken });
});
}
function getSignedDownloadUrl(filename, callback) {
assert.strictEqual(typeof filename, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getSignedDownloadUrl()');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var s3 = new AWS.S3(credentials);
var params = {
Bucket: config.aws().backupBucket,
Key: config.aws().backupPrefix + '/' + filename,
Expires: 60 * 30 /* 30 minutes */
};
var url = s3.getSignedUrl('getObject', params);
callback(null, { url: url, sessionToken: credentials.sessionToken });
});
}
function getZoneByName(zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getZoneByName: %s', zoneName);
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.listHostedZones({}, function (error, result) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
var zone = result.HostedZones.filter(function (zone) {
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
})[0];
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
debug('getZoneByName: found zone', zone);
callback(null, zone);
});
});
}
function addSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('addSubdomain: ' + subdomain + ' for domain ' + zoneName + ' with value ' + value);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var params = {
ChangeBatch: {
Changes: [{
Action: 'UPSERT',
ResourceRecordSet: {
Type: type,
Name: fqdn,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
}
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.code === 'PriorRequestNotComplete') {
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error) {
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('addSubdomain: success. changeInfoId:%j', result);
callback(null, result.ChangeInfo.Id);
});
});
});
}
function delSubdomain(zoneName, subdomain, type, value, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('delSubdomain: %s for domain %s.', subdomain, zoneName);
getZoneByName(zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = config.appFqdn(subdomain);
var resourceRecordSet = {
Name: fqdn,
Type: type,
ResourceRecords: [{
Value: value
}],
Weight: 0,
SetIdentifier: fqdn,
TTL: 1
};
var params = {
ChangeBatch: {
Changes: [{
Action: 'DELETE',
ResourceRecordSet: resourceRecordSet
}]
},
HostedZoneId: zone.Id
};
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.changeResourceRecordSets(params, function(error, result) {
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
debug('delSubdomain: resource record set not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'NoSuchHostedZone') {
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('delSubdomain: invalid change batch. No such record to be deleted.');
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error) {
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
debug('delSubdomain: success');
callback(null);
});
});
});
}
function getChangeStatus(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
if (changeId === '') return callback(null, 'INSYNC');
getAWSCredentials(function (error, credentials) {
if (error) return callback(error);
var route53 = new AWS.Route53(credentials);
route53.getChange({ Id: changeId }, function (error, result) {
if (error) return callback(error);
callback(null, result.ChangeInfo.Status);
});
});
}
+31 -22
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
}; };
var assert = require('assert'), var assert = require('assert'),
aws = require('./aws.js'),
config = require('./config.js'), config = require('./config.js'),
debug = require('debug')('box:backups'), debug = require('debug')('box:backups'),
superagent = require('superagent'), superagent = require('superagent'),
@@ -54,42 +55,50 @@ function getAllPaged(page, perPage, callback) {
}); });
} }
function getBackupUrl(app, appBackupIds, callback) { function getBackupUrl(app, callback) {
assert(!app || typeof app === 'object'); assert(!app || typeof app === 'object');
assert(!appBackupIds || util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupurl'; var filename = '';
if (app) {
filename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
} else {
filename = util.format('backup_%s-v%s.tar.gz', (new Date()).toISOString(), config.version());
}
var data = { aws.getSignedUploadUrl(filename, function (error, result) {
boxVersion: config.version(), if (error) return callback(error);
appId: app ? app.id : null,
appVersion: app ? app.manifest.version : null,
appBackupIds: appBackupIds
};
superagent.put(url).query({ token: config.token() }).send(data).end(function (error, result) { var obj = {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); id: filename,
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text)); url: result.url,
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response')); sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
return callback(null, result.body); debug('getBackupUrl: ', obj);
callback(null, obj);
}); });
} }
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreUrl(backupId, callback) { function getRestoreUrl(backupId, callback) {
assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/restoreurl'; aws.getSignedDownloadUrl(backupId, function (error, result) {
if (error) return callback(error);
superagent.put(url).query({ token: config.token(), backupId: backupId }).end(function (error, result) { var obj = {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error)); id: backupId,
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text)); url: result.url,
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response')); sessionToken: result.sessionToken,
backupKey: config.backupKey()
};
return callback(null, result.body); debug('getRestoreUrl: ', obj);
callback(null, obj);
}); });
} }
+109 -72
View File
@@ -19,8 +19,7 @@ exports = module.exports = {
reboot: reboot, reboot: reboot,
migrate: migrate, migrate: migrate,
backup: backup, backup: backup,
ensureBackup: ensureBackup ensureBackup: ensureBackup};
};
var apps = require('./apps.js'), var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError, AppsError = require('./apps.js').AppsError,
@@ -40,6 +39,7 @@ var apps = require('./apps.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
SettingsError = settings.SettingsError, SettingsError = settings.SettingsError,
shell = require('./shell.js'), shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'), superagent = require('superagent'),
sysinfo = require('./sysinfo.js'), sysinfo = require('./sysinfo.js'),
tokendb = require('./tokendb.js'), tokendb = require('./tokendb.js'),
@@ -47,7 +47,8 @@ var apps = require('./apps.js'),
user = require('./user.js'), user = require('./user.js'),
UserError = user.UserError, UserError = user.UserError,
userdb = require('./userdb.js'), userdb = require('./userdb.js'),
util = require('util'); util = require('util'),
webhooks = require('./webhooks.js');
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'), var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
@@ -55,7 +56,7 @@ var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'), BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update'; INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
var gAddMailDnsRecordsTimerId = null, var gAddDnsRecordsTimerId = null,
gCloudronDetails = null; // cached cloudron details like region,size... gCloudronDetails = null; // cached cloudron details like region,size...
function debugApp(app, args) { function debugApp(app, args) {
@@ -108,21 +109,18 @@ CloudronError.NOT_FOUND = 'Not found';
function initialize(callback) { function initialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
if (process.env.NODE_ENV !== 'test') { if (process.env.BOX_ENV !== 'test') {
addMailDnsRecords(); addDnsRecords();
} }
// Send heartbeat once we are up and running, this speeds up the Cloudron creation, as otherwise we are bound to the cron.js settings
sendHeartbeat();
callback(null); callback(null);
} }
function uninitialize(callback) { function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function'); assert.strictEqual(typeof callback, 'function');
clearTimeout(gAddMailDnsRecordsTimerId); clearTimeout(gAddDnsRecordsTimerId);
gAddMailDnsRecordsTimerId = null; gAddDnsRecordsTimerId = null;
callback(null); callback(null);
} }
@@ -270,19 +268,20 @@ function getConfig(callback) {
} }
function sendHeartbeat() { function sendHeartbeat() {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat'; // Only send heartbeats after the admin dns record is synced to give appstore a chance to know that fact
debug('Sending heartbeat ' + url); if (!config.get('dnsInSync')) return;
// TODO: this must be a POST var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
if (error) debug('Error sending heartbeat.', error); if (error) debug('Error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text); else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat successful'); else debug('Heartbeat sent to %s', url);
}); });
} }
function sendMailDnsRecordsRequest(callback) { function addDnsRecords() {
assert.strictEqual(typeof callback, 'function'); if (config.get('dnsInSync')) return sendHeartbeat(); // already registered send heartbeat
var DKIM_SELECTOR = 'mail'; var DKIM_SELECTOR = 'mail';
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io'; var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
@@ -290,13 +289,20 @@ function sendMailDnsRecordsRequest(callback) {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public'); var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8'); var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) return callback(new Error('Error reading dkim public key')); if (publicKey === null) {
console.error('Error reading dkim public key. Stop DNS setup.');
return;
}
// remove header, footer and new lines // remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join(''); publicKey = publicKey.split('\n').slice(1, -2).join('');
// note that dmarc requires special DNS records for external RUF and RUA // note that dmarc requires special DNS records for external RUF and RUA
var records = [ var records = [
// naked domain
{ subdomain: '', type: 'A', value: sysinfo.getIp() },
// webadmin domain
{ subdomain: 'my', type: 'A', value: sysinfo.getIp() },
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future // softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' }, { subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
// t=s limits the domainkey to this domain and not it's subdomains // t=s limits the domainkey to this domain and not it's subdomains
@@ -305,38 +311,47 @@ function sendMailDnsRecordsRequest(callback) {
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' } { subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
]; ];
debug('sendMailDnsRecords request:%s', JSON.stringify(records)); debug('addDnsRecords:', records);
superagent subdomains.addMany(records, function (error, changeIds) {
.post(config.apiServerOrigin() + '/api/v1/subdomains')
.set('Accept', 'application/json')
.query({ token: config.token() })
.send({ records: records })
.end(function (error, res) {
if (error) return callback(error);
debug('sendMailDnsRecords status: %s', res.status);
if (res.status === 409) return callback(null); // already registered
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
return callback(null, res.body.ids);
});
}
function addMailDnsRecords() {
if (config.get('mailDnsRecordIds').length !== 0) return; // already registered
sendMailDnsRecordsRequest(function (error, ids) {
if (error) { if (error) {
console.error('Mail DNS record addition failed', error); console.error('Admin DNS record addition failed', error);
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000); gAddDnsRecordsTimerId = setTimeout(addDnsRecords, 10000);
return; return;
} }
debug('Added Mail DNS records successfully'); function checkIfInSync() {
config.set('mailDnsRecordIds', ids); debug('addDnsRecords: Check if admin DNS record is in sync.');
var allDone = true;
async.each(changeIds, function (changeId, callback) {
subdomains.status(changeId, function (error, result) {
if (error) return callback(new Error('Failed to check if admin DNS record is in sync.', error));
if (result !== 'done') allDone = false;
callback(null);
});
}, function (error) {
if (error) console.error(error);
// retry if needed
if (error || !allDone) {
gAddDnsRecordsTimerId = setTimeout(checkIfInSync, 5000);
return;
}
config.set('dnsInSync', true);
// send heartbeat after the dns records are done
sendHeartbeat();
debug('addDnsRecords: done');
});
}
checkIfInSync();
}); });
} }
@@ -418,16 +433,22 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE); var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message)); if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
progress.set(progress.UPDATE, 0, 'Begin ' + (boxUpdateInfo.update ? 'upgrade': 'update'));
// initiate the update/upgrade but do not wait for it // initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) { if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) { doUpgrade(boxUpdateInfo, function (error) {
locker.unlock(locker.OP_BOX_UPDATE); if (error) {
debug('Upgrade failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
}); });
} else { } else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) { doUpdate(boxUpdateInfo, function (error) {
locker.unlock(locker.OP_BOX_UPDATE); if (error) {
debug('Update failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
}); });
} }
@@ -437,17 +458,22 @@ function update(boxUpdateInfo, callback) {
function doUpgrade(boxUpdateInfo, callback) { function doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object'); assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
progress.set(progress.UPDATE, 5, 'Create app and box backup'); function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
backupBoxAndApps(function (error) { backupBoxAndApps(function (error) {
if (error) return callback(error); if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade') superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() }) .query({ token: config.token() })
.send({ version: boxUpdateInfo.version }) .send({ version: boxUpdateInfo.version })
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(new Error('Error making upgrade request: ' + error)); if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return callback(new Error('Server not ready to upgrade: ' + result.body)); if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
progress.set(progress.UPDATE, 10, 'Updating base system'); progress.set(progress.UPDATE, 10, 'Updating base system');
@@ -461,18 +487,23 @@ function doUpgrade(boxUpdateInfo, callback) {
function doUpdate(boxUpdateInfo, callback) { function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
progress.set(progress.UPDATE, 5, 'Create box backup'); function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Create box backup for update');
backupBox(function (error) { backupBox(function (error) {
if (error) return callback(error); if (error) return updateError(error);
// fetch a signed sourceTarballUrl // fetch a signed sourceTarballUrl
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl') superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
.query({ token: config.token(), boxVersion: boxUpdateInfo.version }) .query({ token: config.token(), boxVersion: boxUpdateInfo.version })
.end(function (error, result) { .end(function (error, result) {
if (error) return callback(new Error('Error fetching sourceTarballUrl: ' + error)); if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return callback(new Error('Error fetching sourceTarballUrl status: ' + result.status)); if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return callback(new Error('Error fetching sourceTarballUrl response: ' + result.body)); if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic // NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
var args = { var args = {
@@ -490,15 +521,16 @@ function doUpdate(boxUpdateInfo, callback) {
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'), tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(), isCustomDomain: config.isCustomDomain(),
restoreUrl: null, restoreUrl: null,
restoreKey: null restoreKey: null,
aws: config.aws()
} }
}; };
debug('updating box %j', args); debug('updating box %j', args);
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) { superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error) return callback(error); if (error) return updateError(error);
if (result.status !== 202) return callback(new Error('Error initiating update: ' + result.body)); if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
progress.set(progress.UPDATE, 10, 'Updating cloudron software'); progress.set(progress.UPDATE, 10, 'Updating cloudron software');
@@ -547,22 +579,25 @@ function ensureBackup(callback) {
function backupBoxWithAppBackupIds(appBackupIds, callback) { function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds)); assert(util.isArray(appBackupIds));
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) { backups.getBackupUrl(null /* app */, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message)); if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError.INTERNAL_ERROR, error); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: url %s', result.url); debug('backup: url %s', result.url);
async.series([ async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey ]), shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) { ], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: successful'); debug('backup: successful');
callback(null, result.id); webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
}); });
}); });
} }
@@ -573,7 +608,7 @@ function backupBox(callback) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var appBackupIds = allApps.map(function (app) { return app.lastBackupId; }); var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
appBackupIds = appBackupIds.filter(function (id) { return id !== null }); // remove apps that were never backed up appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupBoxWithAppBackupIds(appBackupIds, callback); backupBoxWithAppBackupIds(appBackupIds, callback);
}); });
@@ -595,25 +630,27 @@ function backupBoxAndApps(callback) {
++processed; ++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) { apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, app.location); progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
if (error && error.reason === AppsError.BAD_STATE) { if (error && error.reason === AppsError.BAD_STATE) {
debugApp(app, 'Skipping backup (istate:%s health%s). Reusing %s', app.installationState, app.health, app.lastBackupId); debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
backupId = app.lastBackupId; backupId = app.lastBackupId;
} }
return iteratorCallback(null, backupId); return iteratorCallback(null, backupId);
}); });
}, function appsBackedUp(error, backupIds) { }, function appsBackedUp(error, backupIds) {
if (error) return callback(error); if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) { backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, ''); progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, restoreKey); callback(error, restoreKey);
}); });
}); });
}); });
} }
+3 -3
View File
@@ -1,6 +1,6 @@
LoadPlugin "table" LoadPlugin "table"
<Plugin table> <Plugin table>
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat"> <Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
Instance "<%= appId %>-memory" Instance "<%= appId %>-memory"
Separator " \\n" Separator " \\n"
<Result> <Result>
@@ -10,7 +10,7 @@ LoadPlugin "table"
</Result> </Result>
</Table> </Table>
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes"> <Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
Instance "<%= appId %>-memory" Instance "<%= appId %>-memory"
Separator "\\n" Separator "\\n"
<Result> <Result>
@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result> </Result>
</Table> </Table>
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat"> <Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
Instance "<%= appId %>-cpu" Instance "<%= appId %>-cpu"
Separator " \\n" Separator " \\n"
<Result> <Result>
+24 -2
View File
@@ -11,8 +11,8 @@ exports = module.exports = {
set: set, set: set,
// ifdefs to check environment // ifdefs to check environment
CLOUDRON: process.env.NODE_ENV === 'cloudron', CLOUDRON: process.env.BOX_ENV === 'cloudron',
TEST: process.env.NODE_ENV === 'test', TEST: process.env.BOX_ENV === 'test',
// convenience getters // convenience getters
apiServerOrigin: apiServerOrigin, apiServerOrigin: apiServerOrigin,
@@ -30,6 +30,9 @@ exports = module.exports = {
isDev: isDev, isDev: isDev,
backupKey: backupKey,
aws: aws,
// for testing resets to defaults // for testing resets to defaults
_reset: initConfig _reset: initConfig
}; };
@@ -70,6 +73,14 @@ function initConfig() {
data.webServerOrigin = null; data.webServerOrigin = null;
data.internalPort = 3001; data.internalPort = 3001;
data.ldapPort = 3002; data.ldapPort = 3002;
data.backupKey = 'backupKey';
data.aws = {
backupBucket: null,
backupPrefix: null,
accessKeyId: null, // selfhosting only
secretAccessKey: null // selfhosting only
};
data.dnsInSync = false;
if (exports.CLOUDRON) { if (exports.CLOUDRON) {
data.port = 3000; data.port = 3000;
@@ -86,6 +97,7 @@ function initConfig() {
name: 'boxtest' name: 'boxtest'
}; };
data.token = 'APPSTORE_TOKEN'; data.token = 'APPSTORE_TOKEN';
data.aws.backupBucket = 'testbucket';
} else { } else {
assert(false, 'Unknown environment. This should not happen!'); assert(false, 'Unknown environment. This should not happen!');
} }
@@ -99,6 +111,9 @@ function initConfig() {
saveSync(); saveSync();
} }
// cleanup any old config file we have for tests
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
initConfig(); initConfig();
// set(obj) or set(key, value) // set(obj) or set(key, value)
@@ -172,3 +187,10 @@ function isDev() {
return /dev/i.test(get('boxVersionsUrl')); return /dev/i.test(get('boxVersionsUrl'));
} }
function backupKey() {
return get('backupKey');
}
function aws() {
return get('aws');
}
+4 -1
View File
@@ -98,12 +98,15 @@ function autoupdatePatternChanged(pattern) {
gAutoupdaterJob = new CronJob({ gAutoupdaterJob = new CronJob({
cronTime: pattern, cronTime: pattern,
onTick: function() { onTick: function() {
debug('Starting autoupdate');
var updateInfo = updateChecker.getUpdateInfo(); var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) { if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.update(updateInfo.box, NOOP_CALLBACK); cloudron.update(updateInfo.box, NOOP_CALLBACK);
} else if (updateInfo.apps) { } else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK); apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
} else {
debug('No auto updates available');
} }
}, },
start: true, start: true,
+16 -1
View File
@@ -7,12 +7,15 @@ exports = module.exports = {
enabled: enabled, enabled: enabled,
setEnabled: setEnabled, setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
}; };
var assert = require('assert'), var assert = require('assert'),
config = require('./config.js'),
tokendb = require('./tokendb.js'), tokendb = require('./tokendb.js'),
settings = require('./settings.js'), settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util'); util = require('util');
function DeveloperError(reason, errorOrMessage) { function DeveloperError(reason, errorOrMessage) {
@@ -68,3 +71,15 @@ function issueDeveloperToken(user, callback) {
callback(null, { token: token, expiresAt: expiresAt }); callback(null, { token: token, expiresAt: expiresAt });
}); });
} }
function getNonApprovedApps(callback) {
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
if (result.status !== 200) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);
});
}
+1 -1
View File
@@ -10,7 +10,7 @@ exports = module.exports = (function () {
var docker; var docker;
var options = connectOptions(); // the real docker var options = connectOptions(); // the real docker
if (process.env.NODE_ENV === 'test') { if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port // test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 }); docker = new Docker({ host: 'http://localhost', port: 5687 });
} else { } else {
+34 -15
View File
@@ -24,6 +24,9 @@ var gLogger = {
fatal: console.error fatal: console.error
}; };
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function start(callback) { function start(callback) {
assert(typeof callback === 'function'); assert(typeof callback === 'function');
@@ -39,15 +42,21 @@ function start(callback) {
result.forEach(function (entry) { result.forEach(function (entry) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron'); var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var tmp = { var tmp = {
dn: dn.toString(), dn: dn.toString(),
attributes: { attributes: {
objectclass: ['user'], objectclass: ['user'],
objectcategory: 'person',
cn: entry.id, cn: entry.id,
uid: entry.id, uid: entry.id,
mail: entry.email, mail: entry.email,
displayname: entry.username, displayname: entry.username,
username: entry.username username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
memberof: groups
} }
}; };
@@ -67,22 +76,32 @@ function start(callback) {
user.list(function (error, result){ user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString())); if (error) return next(new ldap.OperationsError(error.toString()));
// we only have an admin group var groups = [{
var dn = ldap.parseDN('cn=admin,ou=groups,dc=cloudron'); name: 'users',
admin: false
}, {
name: 'admins',
admin: true
}];
var tmp = { groups.forEach(function (group) {
dn: dn.toString(), var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
attributes: { var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
objectclass: ['group'],
cn: 'admin', var tmp = {
memberuid: result.filter(function (entry) { return entry.admin; }).map(function(entry) { return entry.id; }) dn: dn.toString(),
attributes: {
objectclass: ['group'],
cn: group.name,
memberuid: members.map(function(entry) { return entry.id; })
}
};
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
} }
}; });
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
res.send(tmp);
debug('ldap group send:', tmp);
}
res.end(); res.end();
}); });
+22 -6
View File
@@ -9,6 +9,7 @@ function Locker() {
this._operation = null; this._operation = null;
this._timestamp = null; this._timestamp = null;
this._watcherId = -1; this._watcherId = -1;
this._lockDepth = 0; // recursive locks
} }
util.inherits(Locker, EventEmitter); util.inherits(Locker, EventEmitter);
@@ -24,6 +25,7 @@ Locker.prototype.lock = function (operation) {
if (this._operation !== null) return new Error('Already locked for ' + this._operation); if (this._operation !== null) return new Error('Already locked for ' + this._operation);
this._operation = operation; this._operation = operation;
++this._lockDepth;
this._timestamp = new Date(); this._timestamp = new Date();
var that = this; var that = this;
this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5); this._watcherId = setInterval(function () { debug('Lock unreleased %s', that._operation); }, 1000 * 60 * 5);
@@ -35,21 +37,35 @@ Locker.prototype.lock = function (operation) {
return null; return null;
}; };
Locker.prototype.recursiveLock = function (operation) {
if (this._operation === operation) {
++this._lockDepth;
debug('Re-acquired : %s Depth : %s', this._operation, this._lockDepth);
return null;
}
return this.lock(operation);
};
Locker.prototype.unlock = function (operation) { Locker.prototype.unlock = function (operation) {
assert.strictEqual(typeof operation, 'string'); assert.strictEqual(typeof operation, 'string');
if (this._operation !== operation) throw new Error('Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error if (this._operation !== operation) throw new Error('Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
debug('Released : %s', this._operation); if (--this._lockDepth === 0) {
debug('Released : %s', this._operation);
this._operation = null; this._operation = null;
this._timestamp = null; this._timestamp = null;
clearInterval(this._watcherId); clearInterval(this._watcherId);
this._watcherId = -1; this._watcherId = -1;
} else {
debug('Recursive lock released : %s. Depth : %s', this._operation, this._lockDepth);
}
this.emit('unlocked', operation); this.emit('unlocked', operation);
return null; return null;
} };
exports = module.exports = new Locker(); exports = module.exports = new Locker();
+15
View File
@@ -0,0 +1,15 @@
<%if (format === 'text') { %>
New <%= type %> from <%= fqdn %>.
Sender: <%= user.email %>
Sent at: <%= new Date().toUTCString() %>
Subject: <%= subject %>
-----------------------------------------------------------
<%= description %>
<% } else { %>
<% } %>
+24 -1
View File
@@ -15,7 +15,12 @@ exports = module.exports = {
sendCrashNotification: sendCrashNotification, sendCrashNotification: sendCrashNotification,
appDied: appDied appDied: appDied,
FEEDBACK_TYPE_FEEDBACK: 'feedback',
FEEDBACK_TYPE_TICKET: 'ticket',
FEEDBACK_TYPE_APP: 'app',
sendFeedback: sendFeedback
}; };
var assert = require('assert'), var assert = require('assert'),
@@ -277,3 +282,21 @@ function sendCrashNotification(program, context) {
enqueue(mailOptions); enqueue(mailOptions);
} }
function sendFeedback(user, type, subject, description) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof description, 'string');
assert(type === exports.FEEDBACK_TYPE_TICKET || type === exports.FEEDBACK_TYPE_FEEDBACK || type === exports.FEEDBACK_TYPE_APP);
var mailOptions = {
from: config.get('adminEmail'),
to: 'support@cloudron.io',
subject: util.format('[%s] %s - %s', type, config.fqdn(), subject),
text: render('feedback.ejs', { fqdn: config.fqdn(), type: type, user: user, subject: subject, description: description, format: 'text'})
};
enqueue(mailOptions);
}
+1 -1
View File
@@ -25,4 +25,4 @@
</head> </head>
<body> <body class="oauth">
+34 -22
View File
@@ -1,32 +1,42 @@
<% include header %> <% include header %>
<center>
<h1>Login to <%= applicationName %></h1>
</center>
<% if (error) { %>
<center>
<br/><br/>
<h4 class="has-error"><%= error %></h4>
</center>
<% } %>
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6 col-md-offset-3"> <div class="col-md-6 col-md-offset-3">
<form id="loginForm" action="" method="post"> <div class="card">
<input type="hidden" name="_csrf" value="<%= csrf %>"/> <div class="row">
<div class="form-group"> <div class="col-md-12" style="text-align: center;">
<label class="control-label" for="inputUsername">Username or Email</label> <img width="128" height="128" src="<%= applicationLogo %>"/>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required> <h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
<br/>
</div>
</div> </div>
<div class="form-group"> <br/>
<label class="control-label" for="inputPassword">Password</label> <% if (error) { %>
<input type="password" class="form-control" name="password" id="inputPassword" required> <div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div> </div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/> <% } %>
</form> <div class="row">
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a> <div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username or Email</label>
<input type="text" class="form-control" id="inputUsername" name="username" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -34,6 +44,8 @@
<script> <script>
(function () { (function () {
'use strict';
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo; document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo;
+1
View File
@@ -21,6 +21,7 @@ var progress = {
backup: null backup: null
}; };
// We use -1 for percentage to indicate errors
function set(tag, percent, message) { function set(tag, percent, message) {
assert(tag === exports.UPDATE || tag === exports.BACKUP); assert(tag === exports.UPDATE || tag === exports.BACKUP);
assert.strictEqual(typeof percent, 'number'); assert.strictEqual(typeof percent, 'number');
+1 -1
View File
@@ -117,7 +117,7 @@ function installApp(req, res, next) {
if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string')); if ('icon' in data && typeof data.icon !== 'string') return next(new HttpError(400, 'icon is not a string'));
// allow tests to provide an appId for testing // allow tests to provide an appId for testing
var appId = (process.env.NODE_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4(); var appId = (process.env.BOX_ENV === 'test' && typeof data.appId === 'string') ? data.appId : uuid.v4();
debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest); debug('Installing app id:%s storeid:%s loc:%s port:%j restrict:%s manifest:%j', appId, data.appStoreId, data.location, data.portBindings, data.accessRestriction, data.manifest);
+15 -1
View File
@@ -11,13 +11,15 @@ exports = module.exports = {
getConfig: getConfig, getConfig: getConfig,
update: update, update: update,
migrate: migrate, migrate: migrate,
setCertificate: setCertificate setCertificate: setCertificate,
feedback: feedback
}; };
var assert = require('assert'), var assert = require('assert'),
cloudron = require('../cloudron.js'), cloudron = require('../cloudron.js'),
config = require('../config.js'), config = require('../config.js'),
progress = require('../progress.js'), progress = require('../progress.js'),
mailer = require('../mailer.js'),
CloudronError = cloudron.CloudronError, CloudronError = cloudron.CloudronError,
debug = require('debug')('box:routes/cloudron'), debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError, HttpError = require('connect-lastmile').HttpError,
@@ -157,3 +159,15 @@ function setCertificate(req, res, next) {
next(new HttpSuccess(202, {})); next(new HttpSuccess(202, {}));
}); });
} }
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (req.body.type !== mailer.FEEDBACK_TYPE_FEEDBACK && req.body.type !== mailer.FEEDBACK_TYPE_TICKET && req.body.type !== mailer.FEEDBACK_TYPE_APP) return next(new HttpError(400, 'type must be either "ticket", "feedback" or "app"'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
mailer.sendFeedback(req.user, req.body.type, req.body.subject, req.body.description);
next(new HttpSuccess(201, {}));
}
+9 -1
View File
@@ -6,7 +6,8 @@ exports = module.exports = {
enabled: enabled, enabled: enabled,
setEnabled: setEnabled, setEnabled: setEnabled,
status: status, status: status,
login: login login: login,
apps: apps
}; };
var developer = require('../developer.js'), var developer = require('../developer.js'),
@@ -46,3 +47,10 @@ function login(req, res, next) {
}); });
})(req, res, next); })(req, res, next);
} }
function apps(req, res, next) {
developer.getNonApprovedApps(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { apps: result }));
});
}
+33 -20
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
querystring = require('querystring'), querystring = require('querystring'),
util = require('util'), util = require('util'),
session = require('connect-ensure-login'), session = require('connect-ensure-login'),
settings = require('../settings.js'),
tokendb = require('../tokendb'), tokendb = require('../tokendb'),
appdb = require('../appdb'), appdb = require('../appdb'),
url = require('url'), url = require('url'),
@@ -170,6 +171,8 @@ function sendErrorPageOrRedirect(req, res, message) {
} }
} }
// use this instead of sendErrorPageOrRedirect(), in case we have a returnTo provided in the query, to avoid login loops
// This usually happens when the OAuth client ID is wrong
function sendError(req, res, message) { function sendError(req, res, message) {
assert.strictEqual(typeof req, 'object'); assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object'); assert.strictEqual(typeof res, 'object');
@@ -188,37 +191,47 @@ function loginForm(req, res) {
var u = url.parse(req.session.returnTo, true); var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.'); if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
function render(applicationName) { var cloudronName = '';
function render(applicationName, applicationLogo) {
res.render('login', { res.render('login', {
adminOrigin: config.adminOrigin(), adminOrigin: config.adminOrigin(),
csrf: req.csrfToken(), csrf: req.csrfToken(),
cloudronName: cloudronName,
applicationName: applicationName, applicationName: applicationName,
applicationLogo: applicationLogo,
error: req.query.error || null error: req.query.error || null
}); });
} }
clientdb.get(u.query.client_id, function (error, result) { settings.getCloudronName(function (error, name) {
if (error) return sendError(req, res, 'Unknown OAuth client'); if (error) return sendError(req, res, 'Internal Error');
// Handle our different types of oauth clients cloudronName = name;
var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME);
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME);
} else if (appId.indexOf('external-') === 0) {
return render('External Application');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) { clientdb.get(u.query.client_id, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials'); if (error) return sendError(req, res, 'Unknown OAuth client');
var applicationName = result.location || config.fqdn(); // Handle our different types of oauth clients
render(applicationName); var appId = result.appId;
if (appId === constants.ADMIN_CLIENT_ID) {
return render(constants.ADMIN_NAME, '/api/v1/cloudron/avatar');
} else if (appId === constants.TEST_CLIENT_ID) {
return render(constants.TEST_NAME, '/api/v1/cloudron/avatar');
} else if (appId.indexOf('external-') === 0) {
return render('External Application', '/api/v1/cloudron/avatar');
} else if (appId.indexOf('addon-') === 0) {
appId = appId.slice('addon-'.length);
} else if (appId.indexOf('proxy-') === 0) {
appId = appId.slice('proxy-'.length);
}
appdb.get(appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.location || config.fqdn();
render(applicationName, '/api/v1/cloudron/avatar');
});
}); });
}); });
} }
+2 -1
View File
@@ -573,7 +573,8 @@ describe('App installation', function () {
docker.getContainer(appEntry.containerId).inspect(function (error, data) { docker.getContainer(appEntry.containerId).inspect(function (error, data) {
expect(error).to.not.be.ok(); expect(error).to.not.be.ok();
expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ }); expect(data.Config.ExposedPorts['7777/tcp']).to.eql({ });
expect(data.Config.Env).to.contain('ADMIN_ORIGIN=' + config.adminOrigin()); expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1'); expect(data.Config.Env).to.contain('CLOUDRON=1');
clientdb.getByAppId('addon-' + appResult.id, function (error, client) { clientdb.getByAppId('addon-' + appResult.id, function (error, client) {
expect(error).to.not.be.ok(); expect(error).to.not.be.ok();
+3 -3
View File
@@ -50,7 +50,7 @@ function setup(done) {
}, },
function addApp(callback) { function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok' }; var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback); appdb.add('appid', 'appStoreId', manifest, 'location', [ ] /* portBindings */, '' /* accessRestriction */, callback);
} }
], done); ], done);
@@ -119,8 +119,8 @@ describe('Backups API', function () {
it('succeeds', function (done) { it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()) var scope = nock(config.apiServerOrigin())
.put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }) .get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' }); .reply(201, { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' });
request.post(SERVER_URL + '/api/v1/backups') request.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token }) .query({ access_token: token })
+166 -3
View File
@@ -26,6 +26,7 @@ var token = null; // authentication token
var server; var server;
function setup(done) { function setup(done) {
nock.cleanAll();
config.set('version', '0.5.0'); config.set('version', '0.5.0');
server.start(done); server.start(done);
} }
@@ -449,8 +450,18 @@ describe('Cloudron', function () {
}); });
it('fails when in wrong state', function (done) { it('fails when in wrong state', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { size: 'small', region: 'sfo', restoreKey: 'someid' }).reply(409, {}); var scope2 = nock(config.apiServerOrigin())
var scope2 = nock(config.apiServerOrigin()).put('/api/v1/boxes/' + config.fqdn() + '/backupurl?token=APPSTORE_TOKEN', { boxVersion: '0.5.0', appId: null, appVersion: null, appBackupIds: [] }).reply(201, { id: 'someid', url: 'http://foobar', backupKey: 'somerestorekey' }); .get('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', sessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/backupDone?token=APPSTORE_TOKEN')
.reply(200, { id: 'someid' });
var scope1 = nock(config.apiServerOrigin())
.filteringRequestBody(function () { return false; })
.post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', { }).reply(409, {});
injectShellMock(); injectShellMock();
@@ -462,7 +473,7 @@ describe('Cloudron', function () {
expect(result.statusCode).to.equal(202); expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() { function checkAppstoreServerCalled() {
if (scope1.isDone() && scope2.isDone()) { if (scope1.isDone() && scope2.isDone() && scope3.isDone()) {
restoreShellMock(); restoreShellMock();
return done(); return done();
} }
@@ -501,6 +512,158 @@ describe('Cloudron', function () {
}); });
}); });
}); });
describe('feedback', function () {
before(function (done) {
async.series([
setup,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
config._reset();
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
after(cleanup);
it('fails without token', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds with app type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without description', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with feedback type', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without subject', function (done) {
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
}); });
+7 -3
View File
@@ -2,6 +2,10 @@
set -eu -o pipefail set -eu -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
source ${SOURCE_DIR}/setup/INFRA_VERSION
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)" readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)" readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)" readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)"
@@ -20,7 +24,7 @@ start_postgresql() {
docker rm -f postgresql 2>/dev/null 1>&2 || true docker rm -f postgresql 2>/dev/null 1>&2 || true
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh cloudron/postgresql:0.3.0 >/dev/null docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
} }
start_mysql() { start_mysql() {
@@ -36,7 +40,7 @@ start_mysql() {
docker rm -f mysql 2>/dev/null 1>&2 || true docker rm -f mysql 2>/dev/null 1>&2 || true
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh cloudron/mysql:0.3.0 >/dev/null docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
} }
start_mongodb() { start_mongodb() {
@@ -52,7 +56,7 @@ start_mongodb() {
docker rm -f mongodb 2>/dev/null 1>&2 || true docker rm -f mongodb 2>/dev/null 1>&2 || true
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh cloudron/mongodb:0.3.0 >/dev/null docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
} }
start_mysql start_mysql
+11 -2
View File
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Usage: backup.sh <appid> <url> <key>" echo "Usage: backupapp.sh <appid> <url> <key> [aws session token]"
exit 1 exit 1
fi fi
@@ -22,6 +22,7 @@ readonly DATA_DIR="${HOME}/data"
app_id="$1" app_id="$1"
backup_url="$2" backup_url="$2"
backup_key="$3" backup_key="$3"
session_token="$4"
readonly now=$(date "+%Y-%m-%dT%H:%M:%S") readonly now=$(date "+%Y-%m-%dT%H:%M:%S")
readonly app_data_dir="${DATA_DIR}/${app_id}" readonly app_data_dir="${DATA_DIR}/${app_id}"
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}" readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
@@ -31,9 +32,17 @@ btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
for try in `seq 1 5`; do for try in `seq 1 5`; do
echo "Uploading backup to ${backup_url} (try ${try})" echo "Uploading backup to ${backup_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if tar -cvzf - -C "${app_data_snapshot}" . \ if tar -cvzf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \ | openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then | curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
break break
fi fi
cat "${error_log}" && rm "${error_log}" cat "${error_log}" && rm "${error_log}"
+11 -2
View File
@@ -13,12 +13,13 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 2 ]; then if [ $# -lt 2 ]; then
echo "Usage: backupbox.sh <url> <key>" echo "Usage: backupbox.sh <url> <key> [aws session token]"
exit 1 exit 1
fi fi
backup_url="$1" backup_url="$1"
backup_key="$2" backup_key="$2"
session_token="$3"
now=$(date "+%Y-%m-%dT%H:%M:%S") now=$(date "+%Y-%m-%dT%H:%M:%S")
BOX_DATA_DIR="${HOME}/data/box" BOX_DATA_DIR="${HOME}/data/box"
box_snapshot_dir="${HOME}/data/snapshots/box-${now}" box_snapshot_dir="${HOME}/data/snapshots/box-${now}"
@@ -32,9 +33,17 @@ btrfs subvolume snapshot -r "${BOX_DATA_DIR}" "${box_snapshot_dir}"
for try in `seq 1 5`; do for try in `seq 1 5`; do
echo "Uploading backup to ${backup_url} (try ${try})" echo "Uploading backup to ${backup_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
headers=("-H" "Content-Type:")
# federated tokens in CaaS case need session token
if [ ! -z "$session_token" ]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if tar -cvzf - -C "${box_snapshot_dir}" . \ if tar -cvzf - -C "${box_snapshot_dir}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \ | openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -H "Content-Type:" -X PUT --data-binary @- "${backup_url}" 2>"${error_log}"; then | curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
break break
fi fi
cat "${error_log}" && rm "${error_log}" cat "${error_log}" && rm "${error_log}"
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -eu -o pipefail
if [[ $EUID -ne 0 ]]; then
echo "This script should be run as root." >&2
exit 1
fi
if [[ $# == 1 && "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
if [ $# -lt 1 ]; then
echo "Usage: collectlogs.sh <program>"
exit 1
fi
readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --no-pager -u ${program_name} -n 100
echo
echo
echo "dmesg"
echo "-----"
dmesg | tail --lines=100
echo
echo
echo "docker"
echo "------"
journalctl --no-pager -u docker -n 50
echo
echo
+1 -1
View File
@@ -17,7 +17,7 @@ if [[ "$1" == "--check" ]]; then
exit 0 exit 0
fi fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/data/$1" readonly app_data_dir="${HOME}/data/$1"
btrfs subvolume create "${app_data_dir}" btrfs subvolume create "${app_data_dir}"
mkdir -p "${app_data_dir}/data" mkdir -p "${app_data_dir}/data"
+1 -1
View File
@@ -12,7 +12,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0 exit 0
fi fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then if [[ "${BOX_ENV}" == "cloudron" ]]; then
shutdown -r now shutdown -r now
fi fi
+1 -1
View File
@@ -12,7 +12,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0 exit 0
fi fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then if [[ "${BOX_ENV}" == "cloudron" ]]; then
/etc/init.d/collectd restart /etc/init.d/collectd restart
fi fi
+1 -7
View File
@@ -12,12 +12,6 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
exit 0 exit 0
fi fi
if [[ "${OSTYPE}" == "darwin"* ]]; then if [[ "${BOX_ENV}" == "cloudron" ]]; then
# On Mac, brew installs supervisor in /usr/local/bin
export PATH=$PATH:/usr/local/bin
fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then
nginx -s reload nginx -s reload
fi fi
+10 -2
View File
@@ -13,7 +13,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
fi fi
if [ $# -lt 3 ]; then if [ $# -lt 3 ]; then
echo "Usage: restoreapp.sh <appid> <url> <key>" echo "Usage: restoreapp.sh <appid> <url> <key> [aws session token]"
exit 1 exit 1
fi fi
@@ -23,6 +23,7 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
app_id="$1" app_id="$1"
restore_url="$2" restore_url="$2"
restore_key="$3" restore_key="$3"
session_token="$4"
echo "Downloading backup: ${restore_url} and key: ${restore_key}" echo "Downloading backup: ${restore_url} and key: ${restore_key}"
@@ -30,7 +31,14 @@ for try in `seq 1 5`; do
echo "Download backup from ${restore_url} (try ${try})" echo "Download backup from ${restore_url} (try ${try})"
error_log=$(mktemp) error_log=$(mktemp)
if $curl -L "${restore_url}" \ headers=("") # empty element required (http://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u)
# federated tokens in CaaS case need session token
if [[ ! -z "${session_token}" ]]; then
headers=(${headers[@]} "-H" "x-amz-security-token: ${session_token}")
fi
if $curl -L "${headers[@]}" "${restore_url}" \
| openssl aes-256-cbc -d -pass "pass:${restore_key}" \ | openssl aes-256-cbc -d -pass "pass:${restore_key}" \
| tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then | tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then
chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}" chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}"
+1 -1
View File
@@ -17,7 +17,7 @@ if [[ "$1" == "--check" ]]; then
exit 0 exit 0
fi fi
if [[ "${NODE_ENV}" == "cloudron" ]]; then if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly app_data_dir="${HOME}/data/$1" readonly app_data_dir="${HOME}/data/$1"
if [[ -d "${app_data_dir}" ]]; then if [[ -d "${app_data_dir}" ]]; then
find "${app_data_dir}" -mindepth 1 -delete find "${app_data_dir}" -mindepth 1 -delete
+6 -2
View File
@@ -43,7 +43,7 @@ function initializeExpressSync() {
app.set('view options', { layout: true, debug: true }); app.set('view options', { layout: true, debug: true });
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
if (process.env.NODE_ENV === 'test') { if (process.env.BOX_ENV === 'test') {
app.use(express.static(path.join(__dirname, '/../webadmin'))); app.use(express.static(path.join(__dirname, '/../webadmin')));
} else { } else {
app.use(middleware.morgan('dev', { immediate: false })); app.use(middleware.morgan('dev', { immediate: false }));
@@ -92,15 +92,19 @@ function initializeExpressSync() {
router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled); router.post('/api/v1/developer', developerScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.developer.setEnabled);
router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status); router.get ('/api/v1/developer', developerScope, routes.developer.enabled, routes.developer.status);
router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login); router.post('/api/v1/developer/login', routes.developer.enabled, routes.developer.login);
router.get ('/api/v1/developer/apps', developerScope, routes.developer.enabled, routes.developer.apps);
// private routes // private routes
router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig); router.get ('/api/v1/cloudron/config', rootScope, routes.cloudron.getConfig);
router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update); router.post('/api/v1/cloudron/update', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.update);
router.get ('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot); router.post('/api/v1/cloudron/reboot', rootScope, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate); router.post('/api/v1/cloudron/migrate', rootScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate); router.post('/api/v1/cloudron/certificate', rootScope, multipart, routes.cloudron.setCertificate);
router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs); router.get ('/api/v1/cloudron/graphs', rootScope, routes.graphs.getGraphs);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
router.get ('/api/v1/profile', profileScope, routes.user.profile); router.get ('/api/v1/profile', profileScope, routes.user.profile);
router.get ('/api/v1/users', usersScope, routes.user.list); router.get ('/api/v1/users', usersScope, routes.user.list);
+39
View File
@@ -0,0 +1,39 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
util = require('util');
exports = module.exports = SubdomainError;
function SubdomainError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(SubdomainError, Error);
SubdomainError.NOT_FOUND = 'No such domain';
SubdomainError.INTERNAL_ERROR = 'Internal error';
SubdomainError.EXTERNAL_ERROR = 'External error';
SubdomainError.STILL_BUSY = 'Still busy';
SubdomainError.FAILED_TOO_OFTEN = 'Failed too often';
SubdomainError.ALREADY_EXISTS = 'Domain already exists';
SubdomainError.BAD_FIELD = 'Bad Field';
SubdomainError.BAD_STATE = 'Bad State';
SubdomainError.INVALID_ZONE_NAME = 'Invalid domain name';
SubdomainError.INVALID_TASK = 'Invalid task';
+82
View File
@@ -0,0 +1,82 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
async = require('async'),
aws = require('./aws.js'),
config = require('./config.js'),
debug = require('debug')('box:subdomains'),
util = require('util'),
SubdomainError = require('./subdomainerror.js');
module.exports = exports = {
add: add,
addMany: addMany,
remove: remove,
status: status
};
function add(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof record.subdomain, 'string');
assert.strictEqual(typeof record.type, 'string');
assert.strictEqual(typeof record.value, 'string');
assert.strictEqual(typeof callback, 'function');
debug('add: ', record);
aws.addSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
});
}
function addMany(records, callback) {
assert(util.isArray(records));
assert.strictEqual(typeof callback, 'function');
debug('addMany: ', records);
var changeIds = [];
async.eachSeries(records, function (record, callback) {
add(record, function (error, changeId) {
if (error) return callback(error);
changeIds.push(changeId);
callback(null);
});
}, function (error) {
if (error) return callback(error);
callback(null, changeIds);
});
}
function remove(record, callback) {
assert.strictEqual(typeof record, 'object');
assert.strictEqual(typeof callback, 'function');
debug('remove: ', record);
aws.delSubdomain(config.zoneName(), record.subdomain, record.type, record.value, function (error) {
if (error && error.reason !== SubdomainError.NOT_FOUND) return callback(error);
debug('deleteSubdomain: successfully deleted subdomain from aws.');
callback(null);
});
}
function status(changeId, callback) {
assert.strictEqual(typeof changeId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('status: ', changeId);
aws.getChangeStatus(changeId, function (error, status) {
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error));
callback(null, status === 'INSYNC' ? 'done' : 'pending');
});
}
+4 -6
View File
@@ -17,10 +17,7 @@ var appdb = require('./appdb.js'),
var gActiveTasks = { }; var gActiveTasks = { };
var gPendingTasks = [ ]; var gPendingTasks = [ ];
// Task concurrency is 1 for two reasons: var TASK_CONCURRENCY = 5;
// 1. The backup scripts (app and box) turn off swap after finish disregarding other backup processes
// 2. apptask getFreePort has race with multiprocess
var TASK_CONCURRENCY = 1;
var NOOP_CALLBACK = function (error) { console.error(error); }; var NOOP_CALLBACK = function (error) { console.error(error); };
function initialize(callback) { function initialize(callback) {
@@ -54,7 +51,8 @@ function uninitialize(callback) {
function startNextTask() { function startNextTask() {
if (gPendingTasks.length === 0) return; if (gPendingTasks.length === 0) return;
assert(Object.keys(gActiveTasks).length === 0); // since we allow only one task at a time
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
startAppTask(gPendingTasks.shift()); startAppTask(gPendingTasks.shift());
} }
@@ -63,7 +61,7 @@ function startAppTask(appId) {
assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof appId, 'string');
assert(!(appId in gActiveTasks)); assert(!(appId in gActiveTasks));
var lockError = locker.lock(locker.OP_APPTASK); var lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) { if (lockError || Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug('Reached concurrency limit, queueing task for %s', appId); debug('Reached concurrency limit, queueing task for %s', appId);
+22 -19
View File
@@ -2,21 +2,24 @@
set -eu set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source ${SOURCE_DIR}/setup/INFRA_VERSION
# reset sudo timestamp to avoid wrong success # reset sudo timestamp to avoid wrong success
sudo -k || sudo --reset-timestamp sudo -k || sudo --reset-timestamp
# checks if all scripts are sudo access # checks if all scripts are sudo access
scripts=("${SOURCE_DIR}/scripts/rmappdir.sh" \ scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
"${SOURCE_DIR}/scripts/createappdir.sh" \ "${SOURCE_DIR}/src/scripts/createappdir.sh" \
"${SOURCE_DIR}/scripts/reloadnginx.sh" \ "${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
"${SOURCE_DIR}/scripts/backupbox.sh" \ "${SOURCE_DIR}/src/scripts/backupbox.sh" \
"${SOURCE_DIR}/scripts/backupapp.sh" \ "${SOURCE_DIR}/src/scripts/backupapp.sh" \
"${SOURCE_DIR}/scripts/restoreapp.sh" \ "${SOURCE_DIR}/src/scripts/restoreapp.sh" \
"${SOURCE_DIR}/scripts/reboot.sh" \ "${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/scripts/backupswap.sh" \ "${SOURCE_DIR}/src/scripts/backupswap.sh" \
"${SOURCE_DIR}/scripts/reloadcollectd.sh") "${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh")
for script in "${scripts[@]}"; do for script in "${scripts[@]}"; do
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
@@ -24,7 +27,7 @@ for script in "${scripts[@]}"; do
echo "${script} does not have sudo access." echo "${script} does not have sudo access."
echo "You have to add the lines below to /etc/sudoers.d/yellowtent." echo "You have to add the lines below to /etc/sudoers.d/yellowtent."
echo "" echo ""
echo "Defaults!${script} env_keep=\"HOME NODE_ENV\"" echo "Defaults!${script} env_keep=\"HOME BOX_ENV\""
echo "${USER} ALL=(ALL) NOPASSWD: ${script}" echo "${USER} ALL=(ALL) NOPASSWD: ${script}"
echo "" echo ""
exit 1 exit 1
@@ -36,23 +39,23 @@ if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
exit 1 exit 1
fi fi
if ! docker inspect cloudron/redis:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/redis:0.3.0 for tests to run" echo "docker pull ${REDIS_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/mysql:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/mysql:0.3.0 for tests to run" echo "docker pull ${MYSQL_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/postgresql:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/postgresql:0.3.0 for tests to run" echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
exit 1 exit 1
fi fi
if ! docker inspect cloudron/mongodb:0.3.0 >/dev/null 2>/dev/null; then if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull cloudron/mongodb:0.3.0 for tests to run" echo "docker pull ${MONGODB_IMAGE} for tests to run"
exit 1 exit 1
fi fi
+263
View File
@@ -0,0 +1,263 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
require('supererror', { splatchError: true});
var database = require('../database.js'),
expect = require('expect.js'),
EventEmitter = require('events').EventEmitter,
async = require('async'),
user = require('../user.js'),
config = require('../config.js'),
ldapServer = require('../ldap.js'),
ldap = require('ldapjs');
var USER_0 = {
username: 'foobar0',
password: 'password0',
email: 'foo0@bar.com'
};
var USER_1 = {
username: 'foobar1',
password: 'password1',
email: 'foo1@bar.com'
};
function setup(done) {
async.series([
database.initialize.bind(null),
database._clear.bind(null),
ldapServer.start.bind(null),
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, true, null),
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0)
], done);
}
function cleanup(done) {
database._clear(done);
}
describe('Ldap', function () {
before(setup);
after(cleanup);
describe('bind', function () {
it('fails for nonexisting user', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=doesnotexist,ou=users,dc=cloudron', 'password', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
done();
});
});
it('fails with wrong password', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', 'wrongpassword', function (error) {
expect(error).to.be.a(ldap.InvalidCredentialsError);
done();
});
});
it('succeeds', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
expect(error).to.be(null);
done();
});
});
});
describe('search users', function () {
it ('fails for non existing tree', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '(&(l=Seattle)(email=*@foo.com))'
};
client.search('o=example', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
result.on('error', function (error) {
expect(error).to.be.a(ldap.NoSuchObjectError);
done();
});
result.on('end', function (result) {
done(new Error('Should not succeed. Status ' + result.status));
});
});
});
it ('succeeds with basic filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectcategory=person'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[1].username).to.equal(USER_1.username);
done();
});
});
});
it ('succeeds with username wildcard filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectcategory=person)(username=foobar*)'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[1].username).to.equal(USER_1.username);
done();
});
});
});
it ('succeeds with username filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectcategory=person)(username=' + USER_0.username + ')'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].username).to.equal(USER_0.username);
expect(entries[0].memberof.length).to.equal(2);
done();
});
});
});
});
describe('search groups', function () {
it ('succeeds with basic filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectclass=group'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.username);
done();
});
});
});
it ('succeeds with cn wildcard filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(cn=*)'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.username);
done();
});
});
});
it('succeeds with memberuid filter', function (done) {
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(memberuid=' + USER_1.username + ')'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
done();
});
});
});
});
});
+47
View File
@@ -0,0 +1,47 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
backupDone: backupDone
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:webhooks'),
superagent = require('superagent'),
util = require('util');
function backupDone(filename, app, appBackupIds, callback) {
assert.strictEqual(typeof filename, 'string');
assert(!app || typeof app === 'object');
assert(!appBackupIds || util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
debug('backupDone():', filename);
// CaaS
if (config.token()) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupDone';
var data = {
boxVersion: config.version(),
restoreKey: filename,
appId: app ? app.id : null,
appVersion: app ? app.manifest.version : null,
appBackupIds: appBackupIds
};
superagent.post(url).send(data).query({ token: config.token() }).end(function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new Error(result.text));
if (!result.body) return callback(new Error('Unexpected response'));
debug('backupDone()', filename);
return callback(null);
});
} else {
// TODO call custom webhook
callback(null);
}
}
+8 -14
View File
@@ -6,13 +6,13 @@
<title> Cloudron App Error </title> <title> Cloudron App Error </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- external fonts and CSS --> <!-- external fonts and CSS -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- jQuery--> <!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script> <script src="3rdparty/js/jquery.min.js"></script>
@@ -30,20 +30,14 @@
// create main application module // create main application module
var app = angular.module('Application', []); var app = angular.module('Application', []);
// FIXME this does not work with custom domains!
function detectApiOrigin() {
var host = window.location.host;
var tmp = host.split('.')[0];
if (tmp.indexOf('-') === -1) return 'https://my-' + host;
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
}
app.controller('Controller', ['$scope', '$http', function ($scope, $http) { app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.apiOrigin = detectApiOrigin(); var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
$scope.cloudronName = 'Cloudron'; $scope.cloudronName = 'Cloudron';
$scope.referrer = search.referrer || null;
// try to fetch cloudron status // try to fetch cloudron status
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) { $http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data); if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName; $scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' App Error'; document.title = $scope.cloudronName + ' App Error';
@@ -64,7 +58,7 @@
<h1> {{cloudronName}} </h1> <h1> {{cloudronName}} </h1>
<h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3> <h3> <i class="fa fa-frown-o fa-fw text-danger"></i> Something has gone wrong </h3>
This app is currently not running. Please retry later. This app is currently not running. <a href="{{ referrer }}">Please retry later</a>.
<footer> <footer>
<span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span> <span class="text-muted"><a href="mailto: support@cloudron.io">Contact Support</a> - Copyright &copy; Cloudron 2014-15</span>
+6 -15
View File
@@ -6,13 +6,13 @@
<title> Cloudron Error </title> <title> Cloudron Error </title>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- external fonts and CSS --> <!-- external fonts and CSS -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css"> <link href="//fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet" type="text/css">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet" type="text/css">
<!-- jQuery--> <!-- jQuery-->
<script src="3rdparty/js/jquery.min.js"></script> <script src="3rdparty/js/jquery.min.js"></script>
@@ -30,22 +30,13 @@
// create main application module // create main application module
var app = angular.module('Application', []); var app = angular.module('Application', []);
// FIXME this does not work with custom domains!
function detectApiOrigin() {
var host = window.location.host;
var tmp = host.split('.')[0];
if (tmp.indexOf('-') === -1) return 'https://my-' + host;
else return 'https://my' + tmp.slice(tmp.indexOf('-')) + host.slice(tmp.length);
}
app.controller('Controller', ['$scope', '$http', function ($scope, $http) { app.controller('Controller', ['$scope', '$http', function ($scope, $http) {
$scope.apiOrigin = detectApiOrigin();
$scope.cloudronName = 'Cloudron'; $scope.cloudronName = 'Cloudron';
$scope.webServerOriginLink = '/'; $scope.webServerOriginLink = '/';
$scope.errorMessage = ''; $scope.errorMessage = '';
// try to fetch at least config.json to get appstore url // try to fetch at least config.json to get appstore url
$http.get($scope.apiOrigin + '/config.json').success(function(data, status) { $http.get('/config.json').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data); if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.webServerOriginLink = data.webServerOrigin + '/console.html'; $scope.webServerOriginLink = data.webServerOrigin + '/console.html';
}).error(function (data, status) { }).error(function (data, status) {
@@ -54,7 +45,7 @@
}); });
// try to fetch cloudron status // try to fetch cloudron status
$http.get($scope.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) { $http.get('/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return console.error(status, data); if (status !== 200 || typeof data !== 'object') return console.error(status, data);
$scope.cloudronName = data.cloudronName; $scope.cloudronName = data.cloudronName;
document.title = $scope.cloudronName + ' Error'; document.title = $scope.cloudronName + ' Error';
@@ -76,7 +67,7 @@
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<img src="/img/logo_inverted_192.png"/> <img src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/>
<h1> {{cloudronName}} </h1> <h1> {{cloudronName}} </h1>
<div ng-show="errorCode == 0"> <div ng-show="errorCode == 0">
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+10 -8
View File
@@ -6,7 +6,10 @@
<title> Cloudron </title> <title> Cloudron </title>
<link href="/img/favicon.png" rel="icon" type="image/png"> <link href="/api/v1/cloudron/avatar" rel="icon" type="image/png">
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
<!-- Custom Fonts --> <!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css"> <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
@@ -48,9 +51,6 @@
<!-- Main Application --> <!-- Main Application -->
<script src="js/index.js"></script> <script src="js/index.js"></script>
<!-- Theme CSS -->
<link href="theme.css" rel="stylesheet">
</head> </head>
<body> <body>
@@ -81,7 +81,7 @@
<li ng-repeat="change in config.update.box.changelog">{{change}}</li> <li ng-repeat="change in config.update.box.changelog">{{change}}</li>
</ul> </ul>
<br/> <br/>
<fieldset> <fieldset ng-show="installedApps | readyToUpdate">
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off"> <form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }"> <div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label> <label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
@@ -99,7 +99,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button> <button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy" ng-show="installedApps | readyToUpdate"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
</div> </div>
</div> </div>
</div> </div>
@@ -117,6 +117,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand navbar-brand-icon" href="index.html"><img src="/api/v1/cloudron/avatar" width="40" height="40"/></a>
<a class="navbar-brand" href="index.html">{{config.cloudronName || 'Cloudron'}}</a> <a class="navbar-brand" href="index.html">{{config.cloudronName || 'Cloudron'}}</a>
</div> </div>
<!-- /.navbar-header --> <!-- /.navbar-header -->
@@ -142,9 +143,10 @@
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li> <li><a href="#/account"><i class="fa fa-user fa-fw"></i> Account</a></li>
<li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li> <li ng-show="user.admin && config.isDev"><a href="#/dns"><i class="fa fa-wrench fa-fw"></i> DNS Management</a></li>
<li ng-show="user.admin"><a href="#/upgrade"><i class="fa fa-arrow-up fa-fw"></i> Upgrade</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li> <li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/upgrade"><i class="fa fa-arrow-up fa-fw"></i> Upgrade</a></li>
<li><a href="#/support"><i class="fa fa-comment fa-fw"></i> Support</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li> <li><a href="" ng-click="logout($event)"><i class="fa fa-sign-out fa-fw"></i> Logout</a></li>
</ul> </ul>
+11
View File
@@ -63,6 +63,17 @@ angular.module('Application').service('AppStore', ['$http', 'Client', function (
}); });
}; };
AppStore.prototype.getAppByIdAndVersion = function (appId, version, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/apps/' + appId + '/versions/' + version).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
AppStore.prototype.getManifest = function (appId, callback) { AppStore.prototype.getManifest = function (appId, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm')); if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
+24 -2
View File
@@ -32,7 +32,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
return callback(new ClientError(status, data)); return callback(new ClientError(status, data));
} }
if (result.update) window.location.href = '/update.html'; if (result.update && result.update.percent !== -1) window.location.href = '/update.html';
else callback(new ClientError(status, data)); else callback(new ClientError(status, data));
}, function (data, status) { }, function (data, status) {
client.error(data); client.error(data);
@@ -324,6 +324,15 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
}; };
Client.prototype.getNonApprovedApps = function (callback) {
if (!this._config.developerMode) return callback(null, []);
$http.get(client.apiOrigin + '/api/v1/developer/apps').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.apps || []);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getApp = function (appId, callback) { Client.prototype.getApp = function (appId, callback) {
var appFound = null; var appFound = null;
this._installedApps.some(function (app) { this._installedApps.some(function (app) {
@@ -416,7 +425,7 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}; };
Client.prototype.reboot = function (callback) { Client.prototype.reboot = function (callback) {
$http.get(client.apiOrigin + '/api/v1/cloudron/reboot').success(function(data, status) { $http.post(client.apiOrigin + '/api/v1/cloudron/reboot', { }).success(function(data, status) {
if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data)); if (status !== 202 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data); callback(null, data);
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
@@ -460,6 +469,19 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback)); }).error(defaultErrorHandler(callback));
}; };
Client.prototype.feedback = function (type, subject, description, callback) {
var data = {
type: type,
subject: subject,
description: description
};
$http.post(client.apiOrigin + '/api/v1/cloudron/feedback', data).success(function (data, status) {
if (status !== 201) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.createUser = function (username, email, callback) { Client.prototype.createUser = function (username, email, callback) {
var data = { var data = {
username: username, username: username,
+3
View File
@@ -37,6 +37,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/settings', { }).when('/settings', {
controller: 'SettingsController', controller: 'SettingsController',
templateUrl: 'views/settings.html' templateUrl: 'views/settings.html'
}).when('/support', {
controller: 'SupportController',
templateUrl: 'views/support.html'
}).when('/upgrade', { }).when('/upgrade', {
controller: 'UpgradeController', controller: 'UpgradeController',
templateUrl: 'views/upgrade.html' templateUrl: 'views/upgrade.html'
+2 -4
View File
@@ -86,9 +86,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (error && error.statusCode === 401) return $scope.login(); if (error && error.statusCode === 401) return $scope.login();
if (error) return $scope.error(error); if (error) return $scope.error(error);
// check if we are actually updateing
if (Client.getConfig().progress.update) window.location.href = '/update.html';
Client.refreshUserInfo(function (error, result) { Client.refreshUserInfo(function (error, result) {
if (error) return $scope.error(error); if (error) return $scope.error(error);
@@ -122,7 +119,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
// wait till the view has loaded until showing a modal dialog // wait till the view has loaded until showing a modal dialog
Client.onConfig(function (config) { Client.onConfig(function (config) {
if (config.progress.update) { // check if we are actually updating
if (config.progress.update && config.progress.update.percent !== -1) {
window.location.href = '/update.html'; window.location.href = '/update.html';
} }
+45 -15
View File
@@ -28,11 +28,8 @@ app.config(['$routeProvider', function ($routeProvider) {
controller: 'StepController', controller: 'StepController',
templateUrl: 'views/setup/step2.html' templateUrl: 'views/setup/step2.html'
}).when('/step3', { }).when('/step3', {
controller: 'StepController',
templateUrl: 'views/setup/step3.html'
}).when('/step4', {
controller: 'FinishController', controller: 'FinishController',
templateUrl: 'views/setup/step4.html' templateUrl: 'views/setup/step3.html'
}).otherwise({ redirectTo: '/'}); }).otherwise({ redirectTo: '/'});
}]); }]);
@@ -51,27 +48,51 @@ app.service('Wizard', [ function () {
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudfacegreen.png' url: '/img/avatars/rubber-duck.png'
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudfaceturquoise.png' url: '/img/avatars/carrot.png'
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudglassesgreen.png' url: '/img/avatars/cup.png'
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudglassespink.png' url: '/img/avatars/football.png'
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudglassesturquoise.png' url: '/img/avatars/owl.png'
}, { }, {
file: null, file: null,
data: null, data: null,
url: '/img/avatars/cloudglassesyellow.png' url: '/img/avatars/space-rocket.png'
}, {
file: null,
data: null,
url: '/img/avatars/armchair.png'
}, {
file: null,
data: null,
url: '/img/avatars/cap.png'
}, {
file: null,
data: null,
url: '/img/avatars/pan.png'
}, {
file: null,
data: null,
url: '/img/avatars/meat.png'
}, {
file: null,
data: null,
url: '/img/avatars/umbrella.png'
}, {
file: null,
data: null,
url: '/img/avatars/jar.png'
}]; }];
this.avatar = {}; this.avatar = {};
this.avatarBlob = null; this.avatarBlob = null;
@@ -82,8 +103,9 @@ app.service('Wizard', [ function () {
this.avatar = avatar; this.avatar = avatar;
// scale image and get the blob now // scale image and get the blob now. do not use the previewAvatar element here because it is not updated yet
var img = document.getElementById('previewAvatar'); var img = document.createElement('img');
img.src = avatar.data || avatar.url;
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');
canvas.width = 256; canvas.width = 256;
canvas.height = 256; canvas.height = 256;
@@ -122,7 +144,7 @@ app.service('Wizard', [ function () {
return instance; return instance;
}]); }]);
app.controller('StepController', ['$scope', '$location', 'Wizard', function ($scope, $location, Wizard) { app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', function ($scope, $route, $location, Wizard) {
$scope.wizard = Wizard; $scope.wizard = Wizard;
$scope.next = function (page, bad) { $scope.next = function (page, bad) {
@@ -143,7 +165,7 @@ app.controller('StepController', ['$scope', '$location', 'Wizard', function ($sc
}; };
// cheap way to detect if we are in avatar and name selection step // cheap way to detect if we are in avatar and name selection step
if ($('#previewAvatar').get(0) && $('#avatarFileInput').get(0)) { if ($route.current.templateUrl === 'views/setup/step1.html') {
$('#avatarFileInput').get(0).onchange = function (event) { $('#avatarFileInput').get(0).onchange = function (event) {
var fr = new FileReader(); var fr = new FileReader();
fr.onload = function () { fr.onload = function () {
@@ -161,8 +183,16 @@ app.controller('StepController', ['$scope', '$location', 'Wizard', function ($sc
fr.readAsDataURL(event.target.files[0]); fr.readAsDataURL(event.target.files[0]);
}; };
$scope.wizard.setPreviewAvatar($scope.wizard.availableAvatars[0]); // ensure image got loaded before setting the preview avatar
var image = document.createElement('img');
var randomIndex = Math.floor(Math.random() * $scope.wizard.availableAvatars.length);
image.onload = function() {
$scope.$apply(function () { $scope.wizard.setPreviewAvatar($scope.wizard.availableAvatars[randomIndex]); });
image = null;
};
image.src = $scope.wizard.availableAvatars[randomIndex].data || $scope.wizard.availableAvatars[randomIndex].url;
} }
}]); }]);
app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) { app.controller('FinishController', ['$scope', '$location', '$timeout', 'Wizard', 'Client', function ($scope, $location, $timeout, Wizard, Client) {

Some files were not shown because too many files have changed in this diff Show More