Compare commits

..

1768 Commits

Author SHA1 Message Date
Girish Ramakrishnan 12e2e64c22 initialize memoryLimit correctly 2016-09-13 16:59:09 -07:00
Johannes Zellner 10e7f27b16 Actually we need to specify the signatureVersion... 2016-09-13 12:07:09 +02:00
Girish Ramakrishnan f3542dbd55 0.20.2 changes 2016-09-12 13:32:51 -07:00
Girish Ramakrishnan c1bb264065 Set a timeout for superagent
The default is 'no timeout' and it will wait for the response forever.

https://github.com/visionmedia/superagent/issues/17#issuecomment-207742985
2016-09-12 13:06:18 -07:00
Girish Ramakrishnan ce19f480b3 comment out the admin cert api
part of #47
2016-09-12 12:01:30 -07:00
Girish Ramakrishnan 839b4b11ba disable admin_certificate route for now
part of #47
2016-09-12 12:01:22 -07:00
Girish Ramakrishnan 4df3b30ff0 Make ticks dynamic
fixes #43
2016-09-12 11:47:42 -07:00
Girish Ramakrishnan 471cfe1376 use the latest slider.js
this contains the fix in https://github.com/seiyria/angular-bootstrap-slider/pull/136
2016-09-12 08:51:18 -07:00
Johannes Zellner 8de0746ac8 Revert "Use S3 signature versoin 4 to support all regions"
If we set the correct region name, the signature version is selected
automatically

This reverts commit 1e22cc3236.
2016-09-12 14:27:47 +02:00
Girish Ramakrishnan cd94d8f433 Save user certs separately from automatic certs
Fixing the admin cert is a bit more complex since it is used in
setup script as well. Can do that in a later task.

Fixes #44
2016-09-12 01:44:16 -07:00
Girish Ramakrishnan f2a1e19c9b Fix access control display for email apps
Fixes #45
2016-09-11 23:06:28 -07:00
Girish Ramakrishnan 217fcf564c Explicitly mention the default memory limit 2016-09-11 22:06:29 -07:00
Girish Ramakrishnan 55673ebcc3 customAuth apps do not require oauth proxy 2016-09-11 22:04:58 -07:00
Johannes Zellner 1e22cc3236 Use S3 signature versoin 4 to support all regions
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
2016-09-09 20:07:57 +02:00
Johannes Zellner e40a2e8549 Fix link in docs 2016-09-09 14:12:52 +02:00
Girish Ramakrishnan a80302c4e0 0.20.1 changes 2016-09-08 21:45:46 -07:00
Girish Ramakrishnan bbe3ddefc0 customAuth does not require oauth proxy 2016-09-08 21:36:49 -07:00
Girish Ramakrishnan 4405fcc652 Typo 2016-09-08 21:32:39 -07:00
Johannes Zellner 3cb25ce6fd Remove useless message in error.html 2016-09-08 16:09:34 +02:00
Girish Ramakrishnan ad34838f92 Use a different provider for GeoIP 2016-09-07 20:00:44 -07:00
Girish Ramakrishnan e21df0ea92 update shrinkwrap 2016-09-07 18:47:09 -07:00
Girish Ramakrishnan f72b683b76 Add missing momemt-timezone 2016-09-07 18:26:30 -07:00
Girish Ramakrishnan 95b27df635 update manifestformat to 2.4.3 2016-09-07 17:12:07 -07:00
Girish Ramakrishnan a4c2e5f3d2 Disable access control with customAuth flag 2016-09-07 17:06:26 -07:00
Girish Ramakrishnan 3c5fadb1f5 doc: postInstallMessage 2016-09-07 16:00:08 -07:00
Girish Ramakrishnan 486db676c9 Fix the max-height 2016-09-07 14:25:41 -07:00
Girish Ramakrishnan bde9279742 Tweak the post install ui 2016-09-07 14:25:41 -07:00
Johannes Zellner efdd01c4c8 Replace tagline in invite email 2016-09-07 22:14:26 +02:00
Girish Ramakrishnan f23ecd486d OK -> Got it 2016-09-07 13:11:28 -07:00
Girish Ramakrishnan b4c030b02b display post install message after installation
fixes #19
2016-09-07 12:47:14 -07:00
Girish Ramakrishnan a52f1e07ee rename account to postInstall
part of #19
2016-09-07 12:26:24 -07:00
Girish Ramakrishnan 186d0a1156 update manifestformat (for postInstallMessage) 2016-09-07 12:13:51 -07:00
Girish Ramakrishnan 8e71046d28 track ui state with an enumeration 2016-09-07 11:49:44 -07:00
Girish Ramakrishnan 67c56c7daf initialize resourceConstraintVisible 2016-09-07 11:45:45 -07:00
Girish Ramakrishnan d802b88998 delete tokens when deleting a client
fixes #36
2016-09-07 11:10:19 -07:00
Girish Ramakrishnan 2c9425ceea fix debug 2016-09-07 11:06:50 -07:00
Girish Ramakrishnan 72a7d5e854 fix debug module name 2016-09-07 09:24:15 -07:00
Girish Ramakrishnan fbf3a9daad More changes 2016-09-07 09:23:55 -07:00
Girish Ramakrishnan 1fc9e296b4 doc: add note on OS X and Windows 2016-09-07 09:05:24 -07:00
Girish Ramakrishnan cb31af828a doc: mention that only SLDs are supported 2016-09-07 09:02:24 -07:00
Girish Ramakrishnan de9d556b9e fix failing test 2016-09-07 08:46:36 -07:00
Johannes Zellner 9d98f9fcf5 Retry npm install in base image script 2016-09-07 14:19:29 +02:00
Johannes Zellner 5d3dca6b3f Set a timeout for the meta data request
Part of #37
2016-09-07 14:11:29 +02:00
Johannes Zellner 2ce6791771 Make cloudron-installer depend on box-setup service
Part of #37
2016-09-07 12:10:03 +02:00
Girish Ramakrishnan dd91de8cf6 Add an icon to show account info
part of #19
2016-09-07 02:34:20 -07:00
Girish Ramakrishnan 1a0f3f687a Add button to show accounts section 2016-09-07 01:44:14 -07:00
Girish Ramakrishnan 36d48000b6 change wording 2016-09-07 01:13:20 -07:00
Girish Ramakrishnan b9c10a1256 provide warning to configure UI as well (same as install ui) 2016-09-07 01:13:00 -07:00
Girish Ramakrishnan 5014ca7742 Simply check app.oauthProxy
Part of #6
2016-09-07 00:53:13 -07:00
Girish Ramakrishnan 452c976aa6 add more debugs 2016-09-07 00:53:09 -07:00
Girish Ramakrishnan 5b9c8e517a Add oauthProxy support to access restriction UI
Adds 'unrestricted' access control option for apps that do not have
any auth integration

Fixes #6
2016-09-06 23:53:47 -07:00
Girish Ramakrishnan b66ba0a2c7 take oauthProxy parameter in install and configure routes
part of #6
2016-09-06 23:43:27 -07:00
Girish Ramakrishnan 9a7ac4ffb7 Send oauthProxy parameter for install and configure API 2016-09-06 23:36:18 -07:00
Girish Ramakrishnan 408dd61408 Send and receive oauthProxy in REST routes
Part of #6
2016-09-06 23:32:42 -07:00
Girish Ramakrishnan e915e6fd44 doc: Add debugging section 2016-09-06 23:12:52 -07:00
Girish Ramakrishnan ac5cef3c2f do not introspect the value of accessRestriction
if there are no users or groups, it simply means nobody can access it.
(maybe the admin is doing something on the cloudron and does not want
anyone to access it).
2016-09-06 16:37:14 -07:00
Girish Ramakrishnan aa3501c780 doc: clarify accessRestriction 2016-09-06 16:31:20 -07:00
Girish Ramakrishnan 375082c1ae make tests pass 2016-09-06 13:33:12 -07:00
Girish Ramakrishnan 0900d7b824 Fix crash 2016-09-06 12:54:53 -07:00
Johannes Zellner 1b54f5f797 Support both floating and non fixed ips 2016-09-06 15:43:43 +02:00
Johannes Zellner 9fcaebcf98 Return digitalocean floating ip for caas cloudrons in sysinfo 2016-09-06 15:43:43 +02:00
Girish Ramakrishnan fda8afa73d doc: add ListHostedZones and GetChange to policy
fixes #23
2016-09-05 23:45:04 -07:00
Girish Ramakrishnan 6beaa914d1 doc: selfhosting fixes
fixes #26
2016-09-05 22:47:06 -07:00
Girish Ramakrishnan b22fbfd381 doc: add admin section
Fixes #31
2016-09-05 20:54:16 -07:00
Girish Ramakrishnan 84379cb11f doc: usernmanual updates 2016-09-05 20:42:52 -07:00
Girish Ramakrishnan c63c6f793c do not unregister naked domain of non-custom domains only 2016-09-05 18:40:22 -07:00
Girish Ramakrishnan bc839d7f9b Cannot optimize here since we always need a changeId 2016-09-05 18:31:40 -07:00
Girish Ramakrishnan 539b45d3b0 Bypass DNS check for non-custom domains
Part of #27
2016-09-05 17:39:14 -07:00
Girish Ramakrishnan 1098bbfe25 More 0.20.0 changes 2016-09-05 17:24:26 -07:00
Girish Ramakrishnan 203cac2629 Check if DNS entry already exists before updating it
Fixes #27
2016-09-05 17:14:17 -07:00
Girish Ramakrishnan 3aa2ccaef7 remove unused require 2016-09-05 17:09:19 -07:00
Girish Ramakrishnan 90472e1370 remove subdomains.add API 2016-09-05 17:09:00 -07:00
Girish Ramakrishnan ecc9d1bc02 rename subdomains.update to subdomains.upsert 2016-09-05 16:58:13 -07:00
Girish Ramakrishnan 4fc6eb1876 fix route53.get() 2016-09-05 15:17:42 -07:00
Girish Ramakrishnan e82152ac86 Fix failing tests
ec63c1c96e broke tests
2016-09-05 14:04:40 -07:00
Girish Ramakrishnan 348e44e959 Fix wording
Fixes #18
2016-09-05 10:08:33 -07:00
Girish Ramakrishnan ec63c1c96e use subdomains.update to short-circuit dns propagation check
if the entry is already uptodate, then we can bypass the wait
2016-09-05 10:00:15 -07:00
Johannes Zellner 59e1e55666 Use angular bootstrap collapse for advanced app configuration 2016-09-05 13:51:29 +02:00
Girish Ramakrishnan 6b4d906336 remove jslint comment 2016-09-04 23:56:50 -07:00
Girish Ramakrishnan f96fda325d Return SubdomainError.BAD_FIELD in route53 backend
Part of #27
2016-09-04 19:46:46 -07:00
Girish Ramakrishnan 2caf57d2c7 Add SubdomainError.BAD_FIELD
The CaaS backend return 400 for conflicts. We can use this to abort
early if there is a conflict in DNS entries. For example, if an app
subdomain already exists in DNS.

Part of #27
2016-09-04 19:35:19 -07:00
Girish Ramakrishnan 01064323c2 Show install anyway button for dev OR self-hosted
Also, show the upgrade button only for caas cloudrons
2016-09-04 18:12:30 -07:00
Girish Ramakrishnan b7d2b644b3 simplify wording 2016-09-04 18:05:21 -07:00
Girish Ramakrishnan 3bdae8f4ac typo 2016-09-04 18:00:28 -07:00
Girish Ramakrishnan cb14096fbd Make step size 128M 2016-09-04 12:20:38 -07:00
Girish Ramakrishnan 4d6fad79af Send memoryLimit parameter 2016-09-04 12:17:14 -07:00
Girish Ramakrishnan 4a827dcfb3 Allow setting memory limit to 0 for default
Part of #18
2016-09-04 12:17:09 -07:00
Girish Ramakrishnan 31636af643 memoryUsage -> memoryLimit 2016-09-04 11:48:03 -07:00
Girish Ramakrishnan b4bd6500a3 enable memory limit ui
part of #18
2016-09-04 11:39:03 -07:00
Girish Ramakrishnan 7b4c9dffab fix memoryLimit handling
0 means not set. and it will follow value in manifest

part of #18
2016-09-04 11:38:54 -07:00
Girish Ramakrishnan f083d81b35 Fix typo 2016-09-04 11:38:36 -07:00
Girish Ramakrishnan 550f14da6f doc: 256mb is the default memory limit 2016-09-03 19:48:26 -07:00
Girish Ramakrishnan ec33dc99c0 Add 0.20.0 changes 2016-09-03 12:33:53 -07:00
Girish Ramakrishnan d07840c8ef Load mail conf in mailer.js
addon vars is not really used other than for email. mail config
is required even if platform code is not initialized since it
is called from mailer.unexpectedExit

fixes #29
2016-09-03 12:27:12 -07:00
Girish Ramakrishnan 23514078f3 add explicit check for test code 2016-09-03 11:51:32 -07:00
Girish Ramakrishnan a5bb438af4 rename env var 2016-09-03 11:46:57 -07:00
Girish Ramakrishnan 27a36492db mailer.unexpectedExit has no db access
part of #28
2016-09-03 11:35:59 -07:00
Girish Ramakrishnan ef6344afae ignore any error when stopping addon containers
fixes #29
2016-09-03 11:24:16 -07:00
Girish Ramakrishnan 0058eddd22 clarify that apps can still send mail 2016-09-02 10:07:51 -07:00
Girish Ramakrishnan 464869c021 remove warning about MX record
fixes #25
2016-09-02 09:58:09 -07:00
Girish Ramakrishnan b54ba8e511 doc: fix typo 2016-09-01 19:46:03 -07:00
Girish Ramakrishnan d40fb390b7 Fix plan listing
We always show the current plan first
We then show all the bigger sizes
2016-09-01 19:16:44 -07:00
Girish Ramakrishnan 8af7ccfe08 add live chat 2016-09-01 16:50:26 -07:00
Girish Ramakrishnan 59b53d347f display demo user credentials in demo cloudrons 2016-09-01 16:21:12 -07:00
Girish Ramakrishnan 70b63af3c9 pass isDemo parameter to angular ui 2016-09-01 15:56:38 -07:00
Girish Ramakrishnan 94c3ac96f0 reload mail credentials when restarting mail container 2016-09-01 15:54:55 -07:00
Girish Ramakrishnan 9f48f76185 fix version 2016-09-01 09:12:45 -07:00
Girish Ramakrishnan 5b295f8019 Update changelog 2016-08-31 23:55:52 -07:00
Girish Ramakrishnan 1874ea7f58 use username instead of id 2016-08-31 23:41:28 -07:00
Girish Ramakrishnan 61ef3f3efb disallow certain actions in demo mode
* Cannot change password
* Cannot delete user
* Cannot migrate domain or change plan

Fixes #20
2016-08-31 22:39:42 -07:00
Girish Ramakrishnan 997152ad14 Add help section 2016-08-31 22:34:20 -07:00
Girish Ramakrishnan 219bd69e63 parse and save isDemo provision parameter 2016-08-31 22:03:46 -07:00
Girish Ramakrishnan 35b25a4e28 add DEMO_USER_ID 2016-08-31 22:03:41 -07:00
Girish Ramakrishnan d3d7f2c320 Add config.isDemo() 2016-08-31 20:51:36 -07:00
Girish Ramakrishnan e25ad601c7 add 0.20.0 changes 2016-08-31 09:27:43 -07:00
Girish Ramakrishnan a6a61d2586 Fixup the ui code
Fixes #16
2016-08-31 09:20:18 -07:00
Girish Ramakrishnan 42b65baa39 Listen to mail config change event correctly 2016-08-31 09:19:41 -07:00
Girish Ramakrishnan 0eab262084 remove any existing container 2016-08-31 09:19:41 -07:00
Girish Ramakrishnan 11b89f473e s/done/callback 2016-08-31 07:19:24 -07:00
Girish Ramakrishnan 0e935580b6 enable email for existing cloudrons 2016-08-31 05:34:06 -07:00
Girish Ramakrishnan 6d783220fd Add settings UI to enable/disable mail 2016-08-30 22:46:00 -07:00
Girish Ramakrishnan 344908b5b1 add default mailconfig (false) 2016-08-30 22:17:04 -07:00
Girish Ramakrishnan 723de796c7 add get/setMailConfig 2016-08-30 22:07:38 -07:00
Girish Ramakrishnan 546d8ae4e2 re-setup mail container when config key changes
part of #16
2016-08-30 21:54:06 -07:00
Girish Ramakrishnan f3fd2d7950 add mx record during mail container setup
part of #16
2016-08-30 21:36:40 -07:00
Girish Ramakrishnan cbd4903960 remove .js 2016-08-30 21:33:56 -07:00
Girish Ramakrishnan 267141fa9a expose ports in mail container based on mail config setting
part of #16
2016-08-30 21:33:47 -07:00
Girish Ramakrishnan af82af5652 test: mail config get/set
Part of #16
2016-08-30 21:23:03 -07:00
Girish Ramakrishnan d3a5a83f93 doc: get/set email config
Part of #16
2016-08-30 21:11:26 -07:00
Girish Ramakrishnan 5b52eeb573 add route to enable/disable mail
mail is disabled by default

Part of #16
2016-08-30 21:09:22 -07:00
Girish Ramakrishnan db8afaf3ff remove underused first run event 2016-08-30 21:09:05 -07:00
Girish Ramakrishnan 8e76d44a30 Remove dead code
From 69402d0079
2016-08-30 20:59:28 -07:00
Girish Ramakrishnan a86e30b917 fix docs a bit 2016-08-30 17:22:59 -07:00
Girish Ramakrishnan b214bd5d52 Give postgresql more memory
Fixes #14
2016-08-30 17:19:05 -07:00
Johannes Zellner 1f1e299939 Do not offer 'install anyway' 2016-08-30 11:33:32 +02:00
Johannes Zellner 8312cbe792 show settings with migrate ui on upgrade button click 2016-08-30 10:37:25 +02:00
Johannes Zellner a9210dcc0c Document xFrameOptions in api docs 2016-08-29 15:54:47 +02:00
Girish Ramakrishnan 8339e65eb8 Cloudron incorporated in 2015 2016-08-25 15:03:48 -07:00
Girish Ramakrishnan 3ba5bd836b use cloudron.conf to determine if this is an update
see also d60b386bca
2016-08-25 10:32:58 -07:00
Girish Ramakrishnan f1ed4ab20c doc: add alternate backup listing command 2016-08-24 18:45:23 -07:00
Girish Ramakrishnan 22d86ff5b9 create signed urls that are valid for a day
sometimes the downloads take overly long and it's annoying that they
expire so soon.
2016-08-24 17:53:31 -07:00
Girish Ramakrishnan d60b386bca Use cloudron.conf file to determine if this is an update
The installer determines if it an update based on existence of box dir.
It then calls the nginx splash setup code. The splash setup code relies
on the data directory being setup. Otherwise, it barfs in data/nginx.

This results in a case where restarting cloudron-installer when it is
unpacking the box code results in an error about being unable to write
admin.conf (since the data dir gets setup only in box setup.sh).

So, use cloudron.conf to determine if box was setup and running at some
point (this is already used in the js code)
2016-08-23 17:17:24 -07:00
Girish Ramakrishnan b7869a4fdd Fix exec docs to be a GET instead of POST 2016-08-22 14:07:57 -07:00
Girish Ramakrishnan 86903183df Fix routing TCP upgrades via express middleware
Currently, if there was a POST request with 'tcp' upgrade, the code just hangs and waits
till timeout.

Instead, let express code will give us a default 'finalhandler' which responds
appropriately - https://github.com/expressjs/express/blob/master/lib/application.js#L161

https://github.com/pillarjs/finalhandler/blob/master/index.js#L57 for future reference
on how to call this callback should socket.destroy need to be called.
2016-08-22 13:21:46 -07:00
Girish Ramakrishnan e4c2483ae5 upgrade header value is already checked in the route handlers
also, req.end() crashes
2016-08-22 13:21:46 -07:00
Girish Ramakrishnan 36f7e573a8 change base image version 2016-08-21 21:03:15 -07:00
Girish Ramakrishnan 8bebbfbace 0.19.0 changes 2016-08-21 16:45:08 -07:00
Girish Ramakrishnan e198f34219 add note about db upgrades 2016-08-21 15:50:14 -07:00
Girish Ramakrishnan 6a4bda1f7e bump test container 2016-08-21 13:25:27 -07:00
Girish Ramakrishnan 3bf0a392b9 bump mysql version 2016-08-21 13:01:31 -07:00
Girish Ramakrishnan 4165bf35d0 bump mail version 2016-08-20 12:08:02 -07:00
Girish Ramakrishnan fc1a288a2d bump graphite 2016-08-20 11:07:25 -07:00
Girish Ramakrishnan 7f37a9ce50 Bump redis 2016-08-20 11:03:49 -07:00
Girish Ramakrishnan d34f8bc082 bump mongodb 2016-08-20 11:00:08 -07:00
Girish Ramakrishnan 50e598112d doc: mongodb version 2016-08-20 10:59:26 -07:00
Girish Ramakrishnan 8150d1cb8f bump postgresql 2016-08-20 10:42:12 -07:00
Girish Ramakrishnan 5b53280cd4 make baseImage an array 2016-08-20 10:24:29 -07:00
Girish Ramakrishnan 15e6873c14 doc: base image 0.9.0 2016-08-20 10:22:49 -07:00
Girish Ramakrishnan f3978897ae use a different exit code to signal external errors
http://tldp.org/LDP/abs/html/exitcodes.html
2016-08-19 21:54:14 -07:00
Girish Ramakrishnan ba4bb1fd90 box-setup must be run before nginx
nginx configs are in the data volume which get mounted only after
box-setup script.

part of #8
2016-08-19 19:37:44 -07:00
Girish Ramakrishnan bbbc3837b0 box-setup: run before sshd since we modify ssh config files 2016-08-19 19:34:58 -07:00
Girish Ramakrishnan 311e997619 DO: do-resize service has folded into cloud-init 2016-08-19 19:34:12 -07:00
Girish Ramakrishnan 8ee2a7016d installer: retry fetching installer data 5 times
On some VPS providers, getting the userData is "flaky".

Fixes #3
2016-08-19 17:51:14 -07:00
Girish Ramakrishnan 02c5e731a9 add debug log for already provisioned 2016-08-19 17:33:55 -07:00
Girish Ramakrishnan b932a9be10 Set X-Forwarded-Ssl to on
https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#supporting-proxied-ssl
http://stackoverflow.com/questions/16042647/whats-the-de-facto-standard-for-a-reverse-proxy-to-tell-the-backend-ssl-is-used
2016-08-17 17:46:36 -07:00
Girish Ramakrishnan 56618cab23 add docs 2016-08-17 10:59:09 -07:00
Girish Ramakrishnan 2f7fa54fc8 Add API and CLI 2016-08-17 09:41:25 -07:00
Girish Ramakrishnan b538c75f05 Add links to other repos 2016-08-17 09:23:57 -07:00
Girish Ramakrishnan 813950a0e5 Add features 2016-08-17 02:04:19 -07:00
Girish Ramakrishnan 8ef004f7f5 fix README 2016-08-17 00:47:14 -07:00
Girish Ramakrishnan 897326675e Add a logo for gitlab 2016-08-15 22:29:55 -07:00
Girish 06a8508c48 Add license 2016-08-15 19:22:47 +00:00
Girish Ramakrishnan 979f63f3f8 add link to upgrade docs 2016-08-14 19:42:16 -07:00
Girish Ramakrishnan 55ba9a351f mail container bugfix (typo in delay deny patch) 2016-08-13 14:19:52 -07:00
Girish Ramakrishnan bb6ee2b5a0 more 0.18.0 changes 2016-08-13 11:30:26 -07:00
Girish Ramakrishnan b58b350827 Throw exception if dkim keys could not be generated 2016-08-13 00:23:55 -07:00
Girish Ramakrishnan 3bd9fcae6a fix dkim dir perms 2016-08-13 00:23:07 -07:00
Girish Ramakrishnan 020ad746a0 change ownership of box directory 2016-08-12 23:55:20 -07:00
Girish Ramakrishnan b049989eb1 do not change ownership of mail data when updating cloudron
the mail container is still running and changing the ownership behind it's
back causes the mail container to be very upset.
2016-08-12 23:36:41 -07:00
Girish Ramakrishnan c25cc560d8 bump memory for mail container 2016-08-12 19:57:49 -07:00
Girish Ramakrishnan d342652212 bump mail container version (spam support) 2016-08-12 17:17:48 -07:00
Johannes Zellner c30cfefcc5 Reduce LDAP account password length
(This is currently ignored)

256 might be a common db field restriction. At least in openproject
it is based on their table layout.
2016-08-12 21:14:32 +02:00
Girish Ramakrishnan 6cfb8226a9 we are tied to docker 1.10 for now 2016-08-11 16:29:03 -07:00
Girish Ramakrishnan 19fad669f1 Use the unbound dns server
docker filters out the localhost in /etc/resolv.conf by design
and will use the Google DNS nameservers as fallback.

https://docs.docker.com/engine/userguide/networking/configure-dns/
2016-08-11 14:52:34 -07:00
Johannes Zellner 30074ae961 Removing ssh keys has to be done with ssh202 2016-08-11 20:45:49 +02:00
Girish Ramakrishnan 6d5dc0d5c4 0.18.0 changes 2016-08-11 10:52:35 -07:00
Girish Ramakrishnan 7bc5ae17cc Use unbound as nameserver
DO uses Google nameservers by default. This causes RBL queries to fail.

Can be tested with the following command:
$ host 2.0.0.127.zen.spamhaus.org
Host 2.0.0.127.zen.spamhaus.org not found: 3(NXDOMAIN)

With unbound we get:
$ host 2.0.0.127.zen.spamhaus.org
2.0.0.127.zen.spamhaus.org has address 127.0.0.2
2.0.0.127.zen.spamhaus.org has address 127.0.0.10
2.0.0.127.zen.spamhaus.org has address 127.0.0.4

Also, we do not use dnsmasq because it is not a recursive resolver. It will
always forward and this defaults to the value in /etc/network/interfaces
(which is Google DNS on DO!).
2016-08-11 10:32:54 -07:00
Johannes Zellner 65994f307f Make infra_version.js option and fix base image on DO 2016-08-10 12:45:23 +02:00
Johannes Zellner 855bc71ba7 Add changes for 0.17.6 2016-08-05 17:28:24 +02:00
Johannes Zellner f3e842ed45 Retry to acquire a db connection when starting a transaction
This fixes db issues just like we do for regular queries.
Also we now use the .on('connection') to setup the session and db
this is how the docs recommend it
2016-08-05 15:18:32 +02:00
Johannes Zellner 1ec5d8c03b Fix error usage 2016-08-05 14:01:19 +02:00
Johannes Zellner 26a590b827 Name the DatabaseError so we get better logs 2016-08-05 12:30:28 +02:00
Johannes Zellner ed734ef2ae fix tests, purchase api is gone 2016-08-04 16:22:50 +02:00
Johannes Zellner 41ff92f747 Only unpurchase if the app has an appstoreId 2016-08-04 15:53:55 +02:00
Johannes Zellner 8702b4320d Wait for all mysql jobs to be finished 2016-08-04 14:06:52 +02:00
Johannes Zellner 6b4675cca1 Remove the ec2 swappiness setting
This revealed mixed results, overall the burstmode ec2
instances are simply a bit underpowered
2016-08-04 10:56:26 +02:00
Johannes Zellner 15f94a5134 Use the correct result object 2016-08-04 09:46:03 +02:00
Johannes Zellner 9c65fae4ec Also unpurchase on app uninstall 2016-08-04 09:38:00 +02:00
Johannes Zellner 65b4c83b75 Use the correct token when calling the appstore for purchase 2016-08-03 23:07:31 +02:00
Johannes Zellner 568c8fa100 Show appstore login if purchase fails 2016-08-03 22:41:27 +02:00
Johannes Zellner a91f89c7dd Rework the purchase api to use the new rest api on the appstore server 2016-08-03 18:30:41 +02:00
Johannes Zellner dde597742c Do not expose purchase function 2016-08-03 17:57:53 +02:00
Girish Ramakrishnan 42fda25718 use systemctl instead of upstart service 2016-08-02 18:45:20 -07:00
Girish Ramakrishnan 9fd40e506d Add TODO note for relaunching mail container when cert path changes 2016-08-02 18:37:43 -07:00
Girish Ramakrishnan 2e51251cac fix debug message 2016-08-02 18:09:45 -07:00
Girish Ramakrishnan b0286a6f7f updatechecker: ensure box state information is not lost
the box and app update checker run in parallel. be sure not to lose
the box state information.
2016-08-02 17:39:43 -07:00
Girish Ramakrishnan 9a6e55e4ea Add 0.17.5 changes 2016-08-02 14:43:39 -07:00
Girish Ramakrishnan fc589a044d send mail to selfhosted cloudron admins about app died and oom 2016-08-02 14:42:42 -07:00
Girish Ramakrishnan 451c770b5c ACME agreement url has changed 2016-08-02 10:40:17 -07:00
Girish Ramakrishnan c769af2bc3 doc: grammar 2016-08-02 09:53:24 -07:00
Girish Ramakrishnan 4a5bb290a7 make chat a separate section 2016-08-02 09:50:39 -07:00
Girish Ramakrishnan 382aaf8de3 use copyright entity name 2016-08-02 09:50:39 -07:00
Girish Ramakrishnan c3f2b8b843 shrink version text 2016-08-02 09:50:39 -07:00
Johannes Zellner 4b93d87310 Set the fallback provider for old caas Cloudrons 2016-08-02 16:41:28 +02:00
Johannes Zellner 4bb91be7d9 Add changes for 0.17.5 2016-08-02 16:41:14 +02:00
Johannes Zellner 78d4fb3cb5 Support cloudron.io registration in login form 2016-08-02 16:15:00 +02:00
Girish Ramakrishnan 884fd5a224 debug when app got an update 2016-08-01 15:35:58 -07:00
Johannes Zellner 28ee914828 Remove debug logs 2016-08-01 16:10:10 +02:00
Johannes Zellner 2e9680ce68 Handle appstore login with implicit registration 2016-08-01 16:09:30 +02:00
Johannes Zellner 124b952e88 Adjust to changed settings rest api 2016-08-01 15:22:50 +02:00
Johannes Zellner 1f1237e785 Setting the appstore config also deals with appstore registration
- First time it registers the cloudron
- Resetting the account will verify that the Cloudron belongs to this user
- If cloudronId is invalid/unknown we reregister
2016-08-01 15:10:47 +02:00
Johannes Zellner d5644ae3f1 Hide potential other modals when we prompt for appstore login 2016-08-01 13:48:15 +02:00
Johannes Zellner c80b89ae8e Appstore login really belongs to the appstore view 2016-08-01 13:43:39 +02:00
Johannes Zellner 6459c8792a Do not prompt for appstore login with caas cloudrons 2016-08-01 13:39:55 +02:00
Johannes Zellner bf38bb30f3 Mention why one needs to login to the appstore 2016-08-01 13:36:24 +02:00
Johannes Zellner 33e572c49d Show appstore login if we don't have a token yet 2016-08-01 13:28:54 +02:00
Johannes Zellner 46af8d1c90 Move the appstore login to main view controller 2016-08-01 13:22:35 +02:00
Johannes Zellner 30606a55fc Check appstore account only onReady() 2016-08-01 13:17:37 +02:00
Johannes Zellner aedd370e76 Verify the appstore details and register the cloudron with the store 2016-08-01 13:08:05 +02:00
Johannes Zellner f60ff45cb6 Tokens are now valid for a week 2016-08-01 10:14:47 +02:00
Johannes Zellner ce28449734 Remove authorized_keys file after setup is done 2016-07-29 18:43:36 +02:00
Johannes Zellner f0ee52505c Mention our chat in the support page 2016-07-29 17:42:21 +02:00
Johannes Zellner 30e936263c Add twitter to the webadmin footer and make the footer more prominent 2016-07-29 17:37:17 +02:00
Johannes Zellner d0cf698dfa Hide the appstore account setup for now 2016-07-28 18:20:18 +02:00
Johannes Zellner df8910b1e1 Update to angularjs 1.5.8
This fixes the dependency issues with bootstrap
2016-07-28 17:29:25 +02:00
Johannes Zellner 7862fbd7ee Set correct focus for selfhosters in setup wizard 2016-07-28 16:22:20 +02:00
Johannes Zellner cd896a4422 Use persistent appstore login tokens 2016-07-28 15:47:09 +02:00
Johannes Zellner b8a635c638 Add UI components to allow cloudron name changes 2016-07-28 12:56:17 +02:00
Johannes Zellner 690564983f Add cloudron name change api to client.js 2016-07-28 12:41:15 +02:00
Johannes Zellner 4fb3a42319 Return 202 when setting the cloudron name 2016-07-28 12:40:36 +02:00
Johannes Zellner 6d2e52b3b5 Add provider fallback in webadmin
This is a bug and requires an upgrade to set the provider again
2016-07-28 12:10:13 +02:00
Johannes Zellner ced31afe55 Fix label for xframeoptions 2016-07-28 10:55:07 +02:00
Girish Ramakrishnan 5c4be56edb 0.17.4 changes 2016-07-27 20:42:22 -07:00
Girish Ramakrishnan 3595f624de Fix progress text 2016-07-27 20:38:49 -07:00
Girish Ramakrishnan 0e9007e9ef fix debug 2016-07-27 20:11:45 -07:00
Girish Ramakrishnan 971647c986 use the new appstore update route to detect app updates 2016-07-27 19:15:10 -07:00
Girish Ramakrishnan 138829f69b remove appupdate pre-release logic
this all seems very premature since prereleases are not supported in
appstore side
2016-07-27 17:46:58 -07:00
Girish Ramakrishnan e0d4c1adc1 use support instead of admin 2016-07-27 11:48:03 -07:00
Girish Ramakrishnan 03c97d2027 send appVersions when checking for updates 2016-07-27 10:14:10 -07:00
Johannes Zellner 867e875707 Revert "Add basic 404 page"
This reverts commit 3793220dd48356d5fe421312915a8392fcccca0e.
2016-07-27 19:09:43 +02:00
Johannes Zellner 2ac7c15b90 Do not show appstore account in settings for caas 2016-07-27 17:58:33 +02:00
Johannes Zellner dcdca52dbd Add basic 404 page 2016-07-27 17:52:54 +02:00
Johannes Zellner 711814cc2f only perform the appstore account setup on non caas cloudrons 2016-07-27 17:42:44 +02:00
Johannes Zellner c2b57a704d We can't use the AppStore API wrapper due to Client.config()
Fetching the config requires of course an access token...
2016-07-27 17:32:22 +02:00
Johannes Zellner 1106aa6bba Create cloudron.io account on the fly with the same credentials 2016-07-27 17:22:03 +02:00
Johannes Zellner 482a87e994 Add cloudron.io account registration api 2016-07-27 17:21:44 +02:00
Johannes Zellner d65990f780 Improve cloudron store checkbox layout 2016-07-27 17:02:22 +02:00
Johannes Zellner 60c1fb4a93 Add checkbox for appstore account creation 2016-07-27 16:54:54 +02:00
Johannes Zellner 02fcb749aa Use uib-tooltip instead of the non angular aware bootstrap one 2016-07-27 16:52:36 +02:00
Johannes Zellner dfc0598ec9 Wrap all controller setup code with Client.onReady()
This ensures we don't rely on timing for execution against a non
ready Client instance
2016-07-27 16:34:38 +02:00
Johannes Zellner b13dd55fc6 Ensure we only callback once for onReady() 2016-07-27 16:34:15 +02:00
Johannes Zellner 3020071fe4 Provider argument for setup is never used 2016-07-27 14:15:22 +02:00
Johannes Zellner 57d2a3ff6e Show model only for caas so far 2016-07-27 11:20:15 +02:00
Johannes Zellner 6fa414206c Remove admin checks in settings view
We anyways only allow settings to be shown for admins
see settings.js
2016-07-27 11:20:15 +02:00
Johannes Zellner 4619435a2d prepend the apiOrigin in one place 2016-07-27 11:20:15 +02:00
Johannes Zellner ce433932dd Remove obsolete property 2016-07-27 11:20:15 +02:00
Johannes Zellner 4b79af7975 Do not set the global auth header for all but use wrappers instead
Setting it global means we send this to all requests being made through angular
2016-07-27 11:20:15 +02:00
Johannes Zellner 35cb804f00 Show appstore account email instead of id 2016-07-27 11:20:15 +02:00
Johannes Zellner b132b2dc15 Also fetch the appstore account profile 2016-07-27 11:20:15 +02:00
Johannes Zellner 5da766131b Set accessToken for appstore via params 2016-07-27 11:20:15 +02:00
Johannes Zellner 642e5aceed Add AppStore.profile() 2016-07-27 11:20:15 +02:00
Johannes Zellner e8088be586 Remove debug console.log() 2016-07-27 11:20:15 +02:00
Johannes Zellner 2a64764deb Save appstore config after login 2016-07-27 11:20:15 +02:00
Johannes Zellner a8d04028f3 Fix typo 2016-07-27 11:20:15 +02:00
Johannes Zellner 57c7ae3c2b Fetch appstore config in settings view 2016-07-27 11:20:15 +02:00
Johannes Zellner 8165227b0a Keep same style in settings rest api 2016-07-27 11:20:15 +02:00
Johannes Zellner f5af539102 Add webadmin client calls for appstore config 2016-07-27 11:20:15 +02:00
Johannes Zellner 41e1afaf68 Add settings/appstore routes 2016-07-27 11:20:15 +02:00
Johannes Zellner 7361acbec5 Add appstore config in settingsdb 2016-07-27 11:20:15 +02:00
Johannes Zellner adbe862fd3 Add register button for appstore account 2016-07-27 11:20:15 +02:00
Johannes Zellner 7e3628f4c5 Add basic error handling 2016-07-27 11:20:15 +02:00
Johannes Zellner 99af676344 Add AppStore.login()/logout() 2016-07-27 11:20:15 +02:00
Johannes Zellner 9f377cb8fe Add cloudron.io icon 2016-07-27 11:20:15 +02:00
Johannes Zellner da1418c48b Add angular base64 module 2016-07-27 11:20:15 +02:00
Johannes Zellner 84b7d77aa0 Add appstore login form dialog 2016-07-27 11:20:15 +02:00
Johannes Zellner 748e30a6e5 Minor rewording 2016-07-27 11:20:15 +02:00
Johannes Zellner 34453c9dde Add initial section for appstore account view in settings 2016-07-27 11:20:15 +02:00
Girish Ramakrishnan ebe64852be checkout is a noun/adjective. check out is a verb 2016-07-27 01:00:25 -07:00
Girish Ramakrishnan f5c7e993ea 0.17.4 changes 2016-07-27 00:22:08 -07:00
Girish Ramakrishnan b628e2a6c8 add hack for mysql server on ec2 2016-07-27 00:15:08 -07:00
Girish Ramakrishnan 01af6ef23a fix wording in out_of_disk_space mail template 2016-07-26 17:22:58 -07:00
Girish Ramakrishnan 947edfec72 typo: Check "available" and not "used" 2016-07-26 17:10:22 -07:00
Girish Ramakrishnan 159fecc9ce send certificate renewal errors to owner for non-caas 2016-07-26 16:47:58 -07:00
Girish Ramakrishnan 0bf8b94bb4 send outOfDiskSpace mails to owners for non-caas provider 2016-07-26 16:43:14 -07:00
Girish Ramakrishnan d4d07e27c0 send email for certificate renewal error 2016-07-26 16:37:10 -07:00
Girish Ramakrishnan e9e09e66c3 remove unused variables 2016-07-26 16:37:10 -07:00
Girish Ramakrishnan a67b2c7559 warn user that custom domain might overwrite MX record 2016-07-25 21:58:28 -07:00
Girish Ramakrishnan d539f1fec8 Keep menu items alphabetical 2016-07-25 21:49:20 -07:00
Girish Ramakrishnan a3c270c4a1 do not reconfigure apps when infra version has not changed 2016-07-25 18:57:54 -07:00
Girish Ramakrishnan 33c70dad8b fix merging blooper 2016-07-25 16:18:09 -07:00
Girish Ramakrishnan 28ec9d82da assume success if dns server to be down 2016-07-25 15:31:26 -07:00
Girish Ramakrishnan e13075b835 more changelog 2016-07-25 14:38:19 -07:00
Girish Ramakrishnan 03022f0207 Do not send more than 1 oom mail every hour 2016-07-25 14:19:20 -07:00
Girish Ramakrishnan 98facf2a3c delete eventlog older than 7 days 2016-07-25 12:54:27 -07:00
Girish Ramakrishnan 338f4bcdea docker event stream can be null if it errored 2016-07-25 11:39:32 -07:00
Girish Ramakrishnan e46b1a9245 test for app instead of error 2016-07-25 11:37:30 -07:00
Girish Ramakrishnan 129843c0ba debug name of addons that changed 2016-07-25 10:18:35 -07:00
Girish Ramakrishnan b079d688c1 bump mail container to 0.18.0 2016-07-25 10:13:05 -07:00
Girish Ramakrishnan 684aec41cc create all addons on infra upgrade 2016-07-25 10:13:01 -07:00
Girish Ramakrishnan cc26c2b1f1 0.17.3 changes 2016-07-25 00:51:30 -07:00
Girish Ramakrishnan 12915ee169 incremental infra creation 2016-07-25 00:39:57 -07:00
Girish Ramakrishnan e5a34581b1 compare objects instead of just the version
this is in preparation of incremental infra updates. in that case,
an equal infra version is not enough (the images could have changed).
infra version will only signify if containers need to be recreated
wholesum.
2016-07-24 23:19:13 -07:00
Girish Ramakrishnan 5c53aec837 setup mail aliases whenever mail container is created 2016-07-24 23:04:27 -07:00
Girish Ramakrishnan b3a4973348 default xframeoption is sameorigin 2016-07-24 23:00:43 -07:00
Girish Ramakrishnan b45fc46ff3 emit platform ready timer only if platform succeeded 2016-07-24 22:59:47 -07:00
Girish Ramakrishnan 0c014d3e74 minor rewording 2016-07-24 22:48:16 -07:00
Johannes Zellner 520845157f Only poll every 5 sec on update 2016-07-18 14:44:08 +02:00
Johannes Zellner 3193cec6aa Skip caas cloudron details fetching for selfhosted cloudrons 2016-07-18 11:59:32 +02:00
Girish Ramakrishnan 17240c77bf pass error message and not the object 2016-07-17 10:15:14 -07:00
Johannes Zellner 82e8c8cef2 Only adjust swapiness for ec2
On DO the disk I/O seems to be much better so this is not required
2016-07-17 18:54:27 +02:00
Girish Ramakrishnan 263c68f9c2 Add placeholder text for domain name 2016-07-17 09:43:27 -07:00
Girish Ramakrishnan 2ccbd7b8d1 Remove application specific configure link
Most people won't visit it this way and it is just cluttering the UI
2016-07-15 11:23:30 -07:00
Johannes Zellner 3c6c575db9 fixup changes typo 2016-07-15 19:00:30 +02:00
Johannes Zellner 3300c6b47a Make the system use swap only when needed
The default swappiness is 60 on ubuntu. This sets the tendency
to swap out memory pages to be more frequent, which in our case
means swapping out to networked disks and increase the cpu load
a lot, which is especially bad on EC2
2016-07-15 14:07:02 +02:00
Johannes Zellner 679d948857 Add 0.17.2 changes 2016-07-15 13:18:54 +02:00
Johannes Zellner c00267a650 Increase default button color contrast 2016-07-15 12:37:52 +02:00
Johannes Zellner 6d1a382381 Prepare for more advanced config options 2016-07-15 12:35:08 +02:00
Johannes Zellner 8e2f259712 Fix radio button margin 2016-07-15 12:34:50 +02:00
Johannes Zellner 0a85f91175 Add error reporting for xFrameOptions field 2016-07-15 11:45:41 +02:00
Johannes Zellner fe81cad9a2 Add help description 2016-07-15 11:36:12 +02:00
Johannes Zellner 3331d1aa13 Ensure the X-Frame-Options header has a single string argument 2016-07-15 11:26:05 +02:00
Johannes Zellner ae35c20227 Pass down the xFramOptions to the app configure route 2016-07-15 11:18:04 +02:00
Johannes Zellner a49e1b5117 Set xFrameOptions fallback 2016-07-15 11:08:11 +02:00
Johannes Zellner 286f360908 Basic ui to specify xFrameOptions 2016-07-14 17:15:25 +02:00
Johannes Zellner 7f6360361f Fixup the appsdb tests 2016-07-14 16:28:59 +02:00
Johannes Zellner 0d5d54d2d8 Add xFrameOptions to apps and routes 2016-07-14 16:28:59 +02:00
Johannes Zellner 37563ee8cb Add xFrameOptions to appsdb.js 2016-07-14 16:28:59 +02:00
Johannes Zellner e902e11024 Add apps.xFrameOptions column 2016-07-14 16:28:59 +02:00
Johannes Zellner dcb14b452b Validate xFrameOptions in app install 2016-07-14 16:28:59 +02:00
Johannes Zellner 66049a9e2d Support x-frame-options in appconfig.ejs template 2016-07-14 16:28:59 +02:00
Johannes Zellner 4b40084c7f Sorry this test needs even more time for me 2016-07-14 16:28:11 +02:00
Johannes Zellner 33c701ece7 Adjust migrate route tests to new api 2016-07-14 16:17:32 +02:00
Johannes Zellner cfeab2db42 Remove hack to show bootstrap tooltip 2016-07-14 12:48:21 +02:00
Johannes Zellner ebb564f623 Add tooltip to show the exact time in event log 2016-07-14 12:46:52 +02:00
Johannes Zellner d501310dc3 Add angular directives for bootstrap
https://angular-ui.github.io/bootstrap/
2016-07-14 12:46:24 +02:00
Girish Ramakrishnan 0c4772db23 merge website link and author name 2016-07-13 10:55:01 -07:00
Girish Ramakrishnan 21c5033e34 remove unused variable 2016-07-13 10:16:45 -07:00
Johannes Zellner 46d725157f show website and author on one line to give room for last updated 2016-07-13 14:55:05 +02:00
Girish Ramakrishnan b84ce23c12 Add 0.17.1 changes 2016-07-12 16:04:50 -07:00
Girish Ramakrishnan 75889af198 Use latest mail container (outbound dkim crash fix) 2016-07-12 16:03:13 -07:00
Girish Ramakrishnan c0f944c1bf use safe.require instead 2016-07-12 11:37:44 -07:00
Girish Ramakrishnan 743a8650f0 Add ability to setup a ghost account for caas 2016-07-12 11:01:02 -07:00
Johannes Zellner 94ee636254 No need to again check the groups for admin
This is already in user.get() which is attached to req.user
2016-07-12 10:11:04 -07:00
Girish Ramakrishnan 57d2fda14c do not validate dns config in test mode 2016-07-12 09:44:58 -07:00
Girish Ramakrishnan a26168e3cd test: put network name in the end 2016-07-12 09:28:25 -07:00
Girish Ramakrishnan 5deadbfdc7 make prettyDate work for more than 30 days 2016-07-09 13:06:59 -07:00
Girish Ramakrishnan 7b7e3b5950 show "Last updated" 2016-07-09 13:03:40 -07:00
Girish Ramakrishnan bcc1b6343e initialize appId before icon is saved 2016-07-09 12:25:00 -07:00
Girish Ramakrishnan d6e275aaf0 0.17.0 changes 2016-07-08 12:52:00 -07:00
Girish Ramakrishnan 78f0992935 set button action 2016-07-08 12:26:32 -07:00
Girish Ramakrishnan f6dfb70afc give autofocus to domain field 2016-07-08 12:12:55 -07:00
Girish Ramakrishnan 11c530b605 do global replace
Otherwise, this breaks title like "Tiny Tiny RSS"
2016-07-08 11:16:39 -07:00
Girish Ramakrishnan 61af079358 fix debug message 2016-07-08 10:46:40 -07:00
Girish Ramakrishnan 3335936e35 fix cors crash with malformed origin 2016-07-07 16:42:45 -07:00
Girish Ramakrishnan 5a6b5f945d fix fqdn for naked domain 2016-07-07 13:14:15 -07:00
Girish Ramakrishnan 87d54b3883 fix comment 2016-07-07 13:12:53 -07:00
Girish Ramakrishnan 32bce6a9a8 add some debugging info 2016-07-07 12:30:08 -07:00
Girish Ramakrishnan fc932487e5 fix typo 2016-07-07 12:28:13 -07:00
Girish Ramakrishnan 9ad4e61b87 query box status via apiServerOrigin otherwise we hit nxdomain 2016-07-06 16:26:40 -05:00
Girish Ramakrishnan 44e7d87aac setup apiServerOrigin for splash page 2016-07-06 16:26:26 -05:00
Girish Ramakrishnan 2637b740ab fix cloudron.retire 2016-07-06 13:12:09 -05:00
Girish Ramakrishnan 1caf4e9e76 remove the isConfigured check entirely
good thing is that we will not check if the my. cert is valid each
time on start up which will work out well when restoring from
old backups with an outdated cert.
2016-07-06 10:11:54 -05:00
Johannes Zellner f0f01453ec Make the backup cronjob run every 4 hours again
This only calls ensureBackup() which in turn would only
trigger one once per day
2016-07-06 14:54:31 +02:00
Girish Ramakrishnan dc78aab821 remove quoting of the json 2016-07-05 22:50:54 -05:00
Girish Ramakrishnan 9b4a400694 typo 2016-07-05 22:34:51 -05:00
Girish Ramakrishnan d9b61500b3 redirect to new domain if it responds to progress 2016-07-05 22:10:12 -05:00
Girish Ramakrishnan 2d01f2a0e9 pass migrate reason all the way to splash code 2016-07-05 22:04:24 -05:00
Girish Ramakrishnan c3b9ed934d use ng-if since the password has a required tag 2016-07-05 17:40:11 -05:00
Girish Ramakrishnan b49d3bd639 fix grammar 2016-07-05 17:33:53 -05:00
Girish Ramakrishnan 9096e16e37 redirect to update.html when migrating domain 2016-07-05 16:29:27 -05:00
Girish Ramakrishnan 944b3a9da1 route53: do not use appFqdn 2016-07-05 16:28:11 -05:00
Girish Ramakrishnan 8d1ff3140a fix typo 2016-07-05 16:16:43 -05:00
Girish Ramakrishnan 88fd25eff4 init customDomain only if we have an access key 2016-07-05 16:10:34 -05:00
Girish Ramakrishnan 57332eb0ce add some space 2016-07-05 16:01:09 -05:00
Girish Ramakrishnan 6b16ce04ab show dns & certs 2016-07-05 15:52:55 -05:00
Girish Ramakrishnan efba474aa5 add js for migrating domain 2016-07-05 15:24:22 -05:00
Girish Ramakrishnan 0a45f087f2 custom domain migration ui (no javascript yet) 2016-07-05 14:38:38 -05:00
Girish Ramakrishnan 95d7d9192d show the domain name 2016-07-05 14:35:12 -05:00
Girish Ramakrishnan bbe21d36c6 dns config: make form a dialog 2016-07-05 14:15:24 -05:00
Girish Ramakrishnan 82a3ac5382 do not reload when dns config changes 2016-07-05 13:38:24 -05:00
Girish Ramakrishnan 26ce8cf7ac do not redirect for non-custom domain 2016-07-05 12:29:30 -05:00
Girish Ramakrishnan 812a6c7ea2 make certificates section visible for non-custom domains 2016-07-05 12:29:30 -05:00
Girish Ramakrishnan 202af95502 Show credentials only for custom domain 2016-07-05 12:29:26 -05:00
Girish Ramakrishnan ff9fb1912b reword text a bit 2016-07-05 12:16:45 -05:00
Girish Ramakrishnan f2c5d8d016 DNS -> Domain 2016-07-05 12:15:25 -05:00
Girish Ramakrishnan eaeaf92c1a handle BAD_FIELD 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan 70034602c7 handle error 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan dcc6108da1 pass req.body instead of options 2016-07-04 23:32:44 -05:00
Girish Ramakrishnan 6acd01eaae pass domain as part of the config 2016-07-04 23:22:50 -05:00
Girish Ramakrishnan e5baee82e8 one of size, region, domain is allowed in migration route 2016-07-04 23:22:50 -05:00
Girish Ramakrishnan 1126626b51 implement domain migration 2016-07-04 22:30:25 -05:00
Girish Ramakrishnan ab1b5f89a1 validate route53 credentials 2016-07-04 19:42:17 -05:00
Girish Ramakrishnan 21c5491717 bump infra version (for mail container) 2016-07-04 16:17:53 -05:00
Johannes Zellner fb5467d1cd Only show plan selection UI for caas 2016-07-04 16:48:42 +02:00
Johannes Zellner 3f5d974c0c Minor echo to ec2 image building 2016-07-04 14:04:41 +02:00
Johannes Zellner e422357670 Set the correct hostname in start.sh 2016-07-04 10:41:54 +02:00
Johannes Zellner 53d03698ad Setup admin certs if we are configured 2016-07-04 10:18:39 +02:00
Girish Ramakrishnan c8a3af83ff use latest mail container (subaddress alias fix) 2016-07-03 08:37:45 -05:00
Girish Ramakrishnan 2e2b75bab2 dns ui: add collapse button 2016-07-03 08:29:55 -05:00
Girish Ramakrishnan d259a3f326 move developer mode settings into API view 2016-07-02 22:33:02 -05:00
Girish Ramakrishnan a92adf07f4 move support to separate section 2016-07-02 22:22:25 -05:00
Girish Ramakrishnan ff428ba5bf Fix migrate API 2016-07-02 17:48:49 -05:00
Girish Ramakrishnan a5def529bb refactor migrate to take options 2016-07-02 16:03:21 -05:00
Johannes Zellner 78fec9ec9b Adjust changes 2016-07-02 18:53:23 +02:00
Girish Ramakrishnan bd5c1269f6 remove jslint 2016-07-02 11:42:52 -05:00
Girish Ramakrishnan 55e2043eca pass domain argument to cloudron.migrate 2016-07-02 11:23:52 -05:00
Girish Ramakrishnan bfd92bf7ed do not rely on appstore for billing, plan, currency 2016-07-02 10:41:10 -05:00
Girish Ramakrishnan 4983120ae8 rename getCloudronDetails 2016-07-02 10:30:12 -05:00
Girish Ramakrishnan 200ae149a9 handle appstore failure when setting plan 2016-07-02 10:23:00 -05:00
Girish Ramakrishnan a863b8fa22 fix cors test 2016-07-02 06:03:33 -05:00
Girish Ramakrishnan 9315e7eb65 read all params individually 2016-07-01 15:27:36 -05:00
Johannes Zellner 982bfc313c Do not allow so send cookies in cors use case 2016-07-01 20:31:43 +02:00
Girish Ramakrishnan 4aa2ce4501 initialize currency in onReady 2016-06-30 17:40:18 -05:00
Girish Ramakrishnan 15d5ff1c51 list any custom plan 2016-06-30 17:28:40 -05:00
Girish Ramakrishnan 505ede7f42 make entire label clickable 2016-06-30 15:49:13 -05:00
Johannes Zellner 88b2ef65cc Pass the provider setting through to the update call 2016-06-30 19:24:36 +02:00
Johannes Zellner 7fc1126e1f Remove unused installer.sh.ejs 2016-06-30 19:03:40 +02:00
Girish Ramakrishnan 8f7e5f154b send complete plan information (since we do not have id) 2016-06-30 12:00:05 -05:00
Girish Ramakrishnan 412243e656 Fix plan selection 2016-06-30 11:48:06 -05:00
Johannes Zellner f06c218bd1 Give the base image creation instance a name 2016-06-30 16:00:53 +02:00
Johannes Zellner 4149a5908b Only trigger cloudron retire and print any errors, but always succeed 2016-06-30 15:05:18 +02:00
Johannes Zellner e82c33b896 Revert "Increase sysadmin route timeout as stopping might take longer"
This reverts commit 900db217ddb84ab324187ab29bf61e593f824e4a.
2016-06-30 15:01:42 +02:00
Johannes Zellner 9182038d12 Revert "Do not wait for cloudron.target to stop"
This reverts commit dcfe2e4fdbcbd2acb98cefe9b50ef0bb1828eb48.
2016-06-30 15:01:32 +02:00
Johannes Zellner da836d6bbe ami region, image separator is a = 2016-06-30 14:39:30 +02:00
Johannes Zellner 894d63554b Ensure all amis are public and available 2016-06-30 14:25:05 +02:00
Johannes Zellner 568593db93 Use t2.small for EC2 image creation 2016-06-30 11:15:51 +02:00
Johannes Zellner 14983861c0 Do not wait for cloudron.target to stop
This will allow the box code to respond properly to the retire request
2016-06-30 10:52:50 +02:00
Johannes Zellner f319919a4f Increase sysadmin route timeout as stopping might take longer 2016-06-30 10:52:50 +02:00
Girish Ramakrishnan f2c897a87d load webadmin after migration 2016-06-29 23:41:42 -05:00
Girish Ramakrishnan 9c8166a2b8 Add some time information 2016-06-29 23:37:39 -05:00
Girish Ramakrishnan 0642e64ccb fix title of update page for migrations 2016-06-29 23:26:47 -05:00
Girish Ramakrishnan 77bd5bfcbe pass retire reason 2016-06-29 23:24:00 -05:00
Girish Ramakrishnan 9a1392b784 set currency based on config 2016-06-29 19:01:41 -05:00
Girish Ramakrishnan 4dabf7bb26 send currency information 2016-06-29 18:59:50 -05:00
Girish Ramakrishnan 4250a26967 send plan information 2016-06-29 18:57:06 -05:00
Girish Ramakrishnan 14ca94be78 fix typo 2016-06-29 18:24:44 -05:00
Girish Ramakrishnan bcc3b4aee7 Make only current plan bold 2016-06-29 15:27:35 -05:00
Johannes Zellner 4d47c21a74 Also prevent autofill for developer mode modal 2016-06-29 21:33:12 +02:00
Johannes Zellner c75b38ec56 Prevent autofill for planchange password input field 2016-06-29 21:29:28 +02:00
Girish Ramakrishnan 64b59a3047 make current plan bold 2016-06-29 14:11:00 -05:00
Girish Ramakrishnan 3a7eb74e28 match ams instead of ams3 region 2016-06-28 17:23:00 -05:00
Girish Ramakrishnan e64a85150a use ams2 since ams3 is over capacity 2016-06-28 17:20:42 -05:00
Girish Ramakrishnan 4939363296 set font-weight to normal for plans 2016-06-28 16:56:25 -05:00
Girish Ramakrishnan 4be3f484d0 check tag type 2016-06-28 16:56:21 -05:00
Girish Ramakrishnan 9bfbdbba3b handle migrate in update.html 2016-06-28 16:21:22 -05:00
Girish Ramakrishnan 0c3de27c3d better progress message 2016-06-28 16:16:15 -05:00
Girish Ramakrishnan 24e36dc24c clean up code 2016-06-28 16:09:44 -05:00
Girish Ramakrishnan 1fb4c80951 remove bad comment 2016-06-28 16:02:35 -05:00
Girish Ramakrishnan 43193a6394 display currency based on region 2016-06-28 15:58:55 -05:00
Girish Ramakrishnan 66fd20e1ff self-retire after migration call 2016-06-28 15:43:39 -05:00
Girish Ramakrishnan 84c5e7bdeb redirect to update.html after migrate call succeeded 2016-06-28 15:41:34 -05:00
Girish Ramakrishnan c7c6944e5f set migrate progress 2016-06-28 15:34:04 -05:00
Girish Ramakrishnan 823290aa29 remove jslint comment 2016-06-28 15:31:08 -05:00
Girish Ramakrishnan 118f36e115 use ng-checked to preselect current plan 2016-06-28 15:29:28 -05:00
Girish Ramakrishnan 2802d5f49b just use margin-left 2016-06-28 14:53:11 -05:00
Girish Ramakrishnan ad9bb6555b fix button alignment 2016-06-28 14:51:47 -05:00
Girish Ramakrishnan 3ec3f172bb migrate UI fixes 2016-06-28 14:36:19 -05:00
Girish Ramakrishnan b9f0efa778 send password for migrate 2016-06-28 14:02:45 -05:00
Girish Ramakrishnan 41e33e71c8 initial migrate UI 2016-06-28 13:45:50 -05:00
Johannes Zellner ed5ebcbd5c Copy our AMIs to all EC2 regions 2016-06-28 12:54:59 +02:00
Johannes Zellner 914ebcb37d Changes for 0.16.6 2016-06-28 12:14:01 +02:00
Johannes Zellner 8769a1d15b Only backup once per day 2016-06-28 12:13:51 +02:00
Girish Ramakrishnan dac9f29900 Add migrate route 2016-06-27 22:40:41 -05:00
Girish Ramakrishnan eaa2058b10 remove jslint comment 2016-06-27 12:37:15 -05:00
Girish Ramakrishnan 2131c6502c 0.16.5 changes 2016-06-25 00:17:09 -05:00
Girish Ramakrishnan 0ef1cd100a bump mail container 2016-06-25 00:16:18 -05:00
Girish Ramakrishnan 5a48b90adc let the mail addon figure out how to setup aliases file 2016-06-25 00:02:19 -05:00
Girish Ramakrishnan 701a9e964f aliases have username as the "to" 2016-06-25 00:02:19 -05:00
Johannes Zellner 621fb6ddce Ensure root can login via ssh 2016-06-24 15:51:45 +02:00
Johannes Zellner d91fe9223c Dedupe the user.verify*() code 2016-06-23 11:58:10 +02:00
Johannes Zellner 7826bc2b20 Fix client secret overflow 2016-06-23 11:28:13 +02:00
Johannes Zellner 9a5e66739c Show error if client actions don't succeed due to API disabled 2016-06-23 11:25:51 +02:00
Johannes Zellner fd22f0d52b Give error feedback if client name is invalid 2016-06-23 10:34:26 +02:00
Johannes Zellner 1bf869963b Only allow alphanumerics and dash in auth client names 2016-06-23 10:16:02 +02:00
Girish Ramakrishnan d1dab8746e rewrite aliases as well and not just the destination 2016-06-22 23:26:33 -05:00
Girish Ramakrishnan 4adcd947e4 0.16.4 changes 2016-06-22 21:54:39 -05:00
Girish Ramakrishnan b08618288a setup aliases by domain name 2016-06-22 21:47:21 -05:00
Girish Ramakrishnan f9ed725002 wait (practically) forever for admin DNS propagation 2016-06-22 16:00:03 -05:00
Girish Ramakrishnan 8cfbf92adc fix acme prod setting detection 2016-06-22 15:55:53 -05:00
Girish Ramakrishnan eb93903bb8 platform might already by ready 2016-06-22 12:03:08 -05:00
Girish Ramakrishnan 501e1342b6 emit ready event if nothing to do 2016-06-22 11:53:48 -05:00
Girish Ramakrishnan 2a761a52d3 more debugs 2016-06-22 11:48:53 -05:00
Johannes Zellner ce116e56bf Remove webdav specific headers
This is not actually doing anything in that directive
2016-06-22 16:06:11 +02:00
Johannes Zellner ab9745e859 Enable root ssh access on ec2 2016-06-22 14:27:42 +02:00
Johannes Zellner ff4b1fa346 Rename createImage -> createDOImage 2016-06-22 14:07:32 +02:00
Johannes Zellner 02fcee5d98 Remove unused vars in image creation scripts 2016-06-22 14:06:58 +02:00
Johannes Zellner 152589e7dd Do not allow ec2 cloudrons to be upgraded from the UI 2016-06-22 10:21:56 +02:00
Johannes Zellner cc3f21e213 Handle new upgrade error code in routes 2016-06-22 10:21:56 +02:00
Johannes Zellner 61d8767c25 Block self upgrades on non caas cloudrons 2016-06-22 10:21:56 +02:00
Johannes Zellner 3416723129 Fix typo 2016-06-22 10:21:56 +02:00
Johannes Zellner 6477c7b47d Add comment in createEC2Image 2016-06-22 10:21:56 +02:00
Johannes Zellner 99ea4c8c30 Make amis public and available in the regions 2016-06-22 10:21:56 +02:00
Johannes Zellner ef200fcc85 Support s3 backup upload without session tokens 2016-06-22 10:21:56 +02:00
Johannes Zellner c691b75344 Make ami public (still commented) 2016-06-22 10:21:56 +02:00
Johannes Zellner c24ef743f7 Try to autodectect if running on DO or EC2 2016-06-22 10:21:56 +02:00
Johannes Zellner 77ecf1ce22 Also handle 403 status code for non-approved apps 2016-06-22 10:21:56 +02:00
Johannes Zellner c6c36a4f3c Also make box-setup.service depend on cloud-init for ec2 2016-06-22 10:21:56 +02:00
Johannes Zellner 2a3640032f Remove obsolete SELFHOSTED env 2016-06-22 10:21:56 +02:00
Johannes Zellner f0e8915825 Do not copy amis to other regions for now 2016-06-22 10:21:56 +02:00
Johannes Zellner 96dabc5694 Support ec2 user-data 2016-06-22 10:21:56 +02:00
Johannes Zellner abb3d5f0ef Copy the new image to oregon and singapore for now 2016-06-22 10:21:56 +02:00
Johannes Zellner 255d4ea088 Also terminate the instance after image creation 2016-06-22 10:21:56 +02:00
Johannes Zellner 3ac7992686 Wait for instance to come up 2016-06-22 10:21:56 +02:00
Johannes Zellner 822e886347 Take ec2 image snapshot 2016-06-22 10:21:56 +02:00
Johannes Zellner 7b5184f181 Initial script for creating ec2 base images 2016-06-22 10:21:56 +02:00
Girish Ramakrishnan f901728cc9 Fix typo 2016-06-21 16:34:31 -05:00
Girish Ramakrishnan 4f0132b371 0.16.3 changes 2016-06-21 15:19:00 -05:00
Girish Ramakrishnan 3ffc2c0440 wait for 10 minutes before giving up on external domain 2016-06-21 15:15:51 -05:00
Girish Ramakrishnan f84de690ce pass retry options to waitForDns 2016-06-21 15:12:36 -05:00
Girish Ramakrishnan 9f74fead4b make it gender netural 2016-06-21 11:59:26 -05:00
Girish Ramakrishnan c1c1fed605 0.16.2 changes 2016-06-21 11:13:56 -05:00
Girish Ramakrishnan 87584be484 restartAppTask when resuming tasks
We end up with duplicate tasks because the auto installer might
have queued up some pending_install tasks on start up.
2016-06-21 10:56:25 -05:00
Girish Ramakrishnan 5a61c5ba51 platform: ready event 2016-06-21 10:46:02 -05:00
Girish Ramakrishnan fbf7e6804f add note for resuming tasks 2016-06-21 10:33:38 -05:00
Girish Ramakrishnan dfa4d093f7 test: ensure redis does not export the port 2016-06-20 22:59:47 -05:00
Girish Ramakrishnan acfeb85d4a redis: do not publish the port on the host 2016-06-20 22:46:04 -05:00
Girish Ramakrishnan b3ad463470 use debug instead 2016-06-20 22:42:37 -05:00
Girish Ramakrishnan 1e54581c40 clear timer 2016-06-20 22:39:11 -05:00
Girish Ramakrishnan a9b91591b4 Make installationProgress TEXT
Some error messages from apptask can be very long! This cases the db
update to fail. In turn causing the installationState to not be set.
2016-06-20 22:30:17 -05:00
Girish Ramakrishnan b59739ec54 fix typo
this is sad. why didn't jshint catch this?
2016-06-20 21:38:39 -05:00
Girish Ramakrishnan bb9bfd542d fix bug in querying mail server IP 2016-06-20 18:26:22 -05:00
Girish Ramakrishnan 93b7e8d0a7 0.16.1 changes 2016-06-20 15:11:25 -05:00
Girish Ramakrishnan b723f9e768 errored apps can be reconfigured
this is especially true when coming from a restore because the app
always has a good backup anyway.
2016-06-20 15:07:57 -05:00
Girish Ramakrishnan 6bc14ea7e4 resumeTasks only when configured and platform ready 2016-06-20 14:46:52 -05:00
Girish Ramakrishnan cabed28f1e use redisName instead 2016-06-20 13:15:10 -05:00
Girish Ramakrishnan ea74e389d3 make generateToken less stronger to fix UI layout issues 2016-06-20 13:13:13 -05:00
Girish Ramakrishnan c37d914518 platform: rename from legacy to corrupt 2016-06-20 11:59:43 -05:00
Girish Ramakrishnan 86b8fe324e add missing comma 2016-06-20 11:15:25 -05:00
Girish Ramakrishnan 8412db0796 add missing brackets 2016-06-20 09:50:03 -05:00
Girish Ramakrishnan da9c07e369 more 0.16.0 changes 2016-06-18 17:59:09 -05:00
Girish Ramakrishnan 3c305a51ce make sure hostname is unset 2016-06-18 17:58:01 -05:00
Girish Ramakrishnan 3ec29dc9e1 Do not set hostname of app containers
do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
name to look up the internal docker ip. this makes curl from within container fail
Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
Hostname cannot be set with container NetworkMode
2016-06-18 17:53:54 -05:00
Girish Ramakrishnan a3d185e653 fix typo 2016-06-18 14:51:49 -05:00
Girish Ramakrishnan c2da3da035 set force to false explicitly 2016-06-18 13:24:27 -05:00
Girish Ramakrishnan 43c69d4dc6 we have exceeded 100 images 2016-06-18 12:12:11 -05:00
Girish Ramakrishnan 5ba1dd39e7 add missing return 2016-06-18 12:10:30 -05:00
Girish Ramakrishnan 56bc391b38 add ui text for clone 2016-06-17 19:11:29 -05:00
Girish Ramakrishnan 7e93c23110 set the lastBackupId to backup from 2016-06-17 18:41:29 -05:00
Girish Ramakrishnan 7009b9f3ac implement clone 2016-06-17 17:45:14 -05:00
Girish Ramakrishnan 0609a90d2a add CLONE installation state 2016-06-17 16:56:15 -05:00
Girish Ramakrishnan fe62aba4d7 make appdb.add take a data object 2016-06-17 16:43:35 -05:00
Girish Ramakrishnan 224a5f370f 0.16.0 changes 2016-06-17 13:08:35 -05:00
Girish Ramakrishnan 584df9a6da fix redis query 2016-06-17 12:39:29 -05:00
Girish Ramakrishnan f31c43bbc3 fix EPIPE issue by using http instead of superagent 2016-06-17 11:12:20 -05:00
Girish Ramakrishnan 10ebff2edf fix response to say cloudron network 2016-06-17 10:10:18 -05:00
Girish Ramakrishnan cc1755105c do not allow access if app is not found 2016-06-17 10:08:41 -05:00
Girish Ramakrishnan 6c5a3997cb fix clientSecret.length test 2016-06-17 10:07:55 -05:00
Girish Ramakrishnan 2017d668a9 use 128 byte passwords 2016-06-17 09:49:25 -05:00
Girish Ramakrishnan 7e57f31d14 create cloudron network in test 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan de0d42e52f search cloudron network for the apps 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan dbadbd2c4e bump all the container versions 2016-06-17 09:18:15 -05:00
Girish Ramakrishnan d51d2e5131 start addons and apps in the cloudron network
also remove getLinkSync, since we don't use linking anymore
2016-06-17 09:18:10 -05:00
Girish Ramakrishnan be2c7a97b3 Use docker command instead of api to create redis container 2016-06-16 18:02:51 -05:00
Girish Ramakrishnan 2ab13d587a remove dead function 2016-06-16 17:24:38 -05:00
Girish Ramakrishnan f13f24c88d Just use docker commands 2016-06-16 15:41:50 -05:00
Girish Ramakrishnan 91bc45bd4e not sure why this is required but it makes the test more stable 2016-06-16 15:09:46 -05:00
Girish Ramakrishnan a75b9c1428 remove debugs 2016-06-16 14:59:20 -05:00
Girish Ramakrishnan d837e9f679 make test more reliable 2016-06-16 14:50:49 -05:00
Girish Ramakrishnan d5ffa53e70 create an isolated docker network named cloudron
we will move from linked containers to an isolated network. This
has the main advantage that linked containers can be upgraded.
For example, I can update the mysql container independently without
breaking the apps that require it. Apps will only see some minor
downtime and will need to reconnect.
2016-06-16 06:59:47 -07:00
Girish Ramakrishnan fee6f3de0f configure/restoreInstalledApps must always succeed 2016-06-16 06:50:34 -07:00
Girish Ramakrishnan f15f3c9052 node-ify setup_infra.sh 2016-06-15 05:26:40 -07:00
Johannes Zellner a37f87511b Prevent clickjacking by sending X-Frame-Options 2016-06-15 13:10:26 +02:00
Girish Ramakrishnan 069778caca 0.15.3 changes 2016-06-14 14:49:25 -07:00
Girish Ramakrishnan 741fe75def fix progress message 2016-06-14 14:42:29 -07:00
Girish Ramakrishnan f8b402a48e add progress message tooltip 2016-06-14 14:32:14 -07:00
Girish Ramakrishnan f53c0b7700 set radix for parseInt 2016-06-14 14:21:24 -07:00
Girish Ramakrishnan f74310f364 fix title of configure dialog 2016-06-14 14:17:05 -07:00
Girish Ramakrishnan a5a1526023 Allow configure command when in configuration state
Currently, we only allow restore (from backup) and uninstall. If configure
is taking very long (like external domain) and someone wants to reconfigure
we should let them.

We are sort of trying to think of 'reconfigure' as 'retry' in case of
external errors.
2016-06-14 14:13:47 -07:00
Girish Ramakrishnan 26f318477b Do not send crash logs for apptask cancellations 2016-06-14 14:13:47 -07:00
Girish Ramakrishnan 060d9e88ef Pass manifest to backupApp 2016-06-14 11:32:29 -07:00
Girish Ramakrishnan 9cf497a87d 0.15.2 changes 2016-06-13 23:31:35 -07:00
Girish Ramakrishnan b174765992 delete unused addonsa fter backup 2016-06-13 23:07:41 -07:00
Girish Ramakrishnan 9c2d217176 fix typo 2016-06-13 23:04:43 -07:00
Girish Ramakrishnan 3197349058 Fix app backup before updates
we were passing the current manifest to the backup code which meant that
the app version and manifest was incorrect.
2016-06-13 21:19:29 -07:00
Girish Ramakrishnan 5f3378878e remove lastBackupConfig 2016-06-13 19:19:28 -07:00
Girish Ramakrishnan 53cd45496b parse the response 2016-06-13 18:28:51 -07:00
Girish Ramakrishnan 942339435a return config correctly 2016-06-13 18:04:22 -07:00
Girish Ramakrishnan 2bd6519795 add assert 2016-06-13 18:02:57 -07:00
Girish Ramakrishnan 1763c36a0b restore from the backup's config.json
To summarize what we are doing is that restore is simply getting old data and
old code. Config is not changed. If config is required, then it has to come
in the restore REST parameter. Otherwise, there is too much magic.

https://blog.smartserver.io/2016/06/13/app-restore/
2016-06-13 16:54:59 -07:00
Girish Ramakrishnan 2c0eb33625 use apps.getAppConfig when generating config.json 2016-06-13 15:11:49 -07:00
Girish Ramakrishnan 040b9993c7 refactor code into getAppConfig 2016-06-13 15:07:15 -07:00
Girish Ramakrishnan 8f21126697 add a way to get the restore config (config.json) 2016-06-13 15:04:27 -07:00
Girish Ramakrishnan 716d29165c store altDomain in backupConfig 2016-06-13 13:14:04 -07:00
Girish Ramakrishnan a2ec308155 pass the lastBackupId explicity as the backup to restore to 2016-06-13 10:13:54 -07:00
Girish Ramakrishnan b82610ba00 pass data argument to restore 2016-06-13 10:08:58 -07:00
Johannes Zellner ed4674cd14 Disable the trial popup 2016-06-13 16:51:28 +02:00
Johannes Zellner 4e9dc75a37 Replace DatabaseError with ClientsError where applicable 2016-06-13 14:43:56 +02:00
Johannes Zellner f284b4cd83 Use clients.get() instead of clientdb.get() 2016-06-13 13:51:14 +02:00
Johannes Zellner 15cf83b37c Fixup the built-in client setup for tests 2016-06-13 13:48:43 +02:00
Johannes Zellner 0eff8911ee Do not use DatabaseError in routes clients.js 2016-06-13 13:29:39 +02:00
Girish Ramakrishnan 814a0ce3a6 wait for 10mins before sending out emails about app being down 2016-06-11 18:36:38 -07:00
Girish Ramakrishnan b3e1c221b7 bump infra to force a reconfigure for existing pr cloudrons 2016-06-11 13:49:55 -07:00
Girish Ramakrishnan dc31946e50 move webdav block outside location
when inside location, nginx is redirecting to 127.0.0.1 (no clue why)
2016-06-11 12:05:16 -07:00
Girish Ramakrishnan 36bbb98970 0.15.1 changes 2016-06-10 12:40:48 -07:00
Girish Ramakrishnan ea4cea9733 add tag on blur 2016-06-10 12:35:57 -07:00
Girish Ramakrishnan 7c06937a57 Add @fqdn hint to email aliases 2016-06-10 12:04:40 -07:00
Girish Ramakrishnan 597704d3ed remove the plain input email aliases 2016-06-10 11:58:25 -07:00
Girish Ramakrishnan 63290b9936 Final fixes to taginput 2016-06-10 11:58:00 -07:00
Girish Ramakrishnan 324222b040 Fix template code style 2016-06-09 16:11:57 -07:00
Girish Ramakrishnan f37b92da04 taginput: remove autocomplete. we don't use it 2016-06-09 10:34:42 -07:00
Girish Ramakrishnan 0de3b8fbdb taginput: add tag input control for email aliases 2016-06-09 10:34:29 -07:00
Johannes Zellner f0cb3f94cb Fixup the token ui to allow removal of user added clients 2016-06-09 16:17:14 +02:00
Johannes Zellner 1508a5c6b9 Add tokendb.delByClientId() 2016-06-09 15:42:52 +02:00
Johannes Zellner 9b9db6acf1 Only the rest api shall not allow to remove those 2016-06-09 15:35:46 +02:00
Johannes Zellner 001bf94773 Remove unused require 2016-06-09 15:35:20 +02:00
Johannes Zellner 0160c12965 Allow to distinguish between built-in auth clients and external ones 2016-06-09 15:35:00 +02:00
Johannes Zellner d08397336d Add test for addon auth clients deletion 2016-06-09 15:12:30 +02:00
Johannes Zellner 880754877d Prevent the rest api to delete addon auth clients 2016-06-09 14:44:38 +02:00
Johannes Zellner 984a191e4c Use the variable correctly 2016-06-09 14:24:53 +02:00
Girish Ramakrishnan cdca43311b wording and other minor fixes 2016-06-08 17:06:27 -07:00
Girish Ramakrishnan 020b47841a 0.15.1 changes 2016-06-08 16:42:00 -07:00
Girish Ramakrishnan 3f602c8a04 verifyWithUsername and not as id 2016-06-08 15:54:22 -07:00
Girish Ramakrishnan dea0c5642d bump mail container 2016-06-08 13:16:44 -07:00
Girish Ramakrishnan 3d2b75860b minor fixes to session ui 2016-06-08 13:04:22 -07:00
Girish Ramakrishnan 0da754a14b fix singular/plural 2016-06-08 12:52:19 -07:00
Girish Ramakrishnan 3d408c8c90 fix wording 2016-06-08 12:43:15 -07:00
Girish Ramakrishnan 40348a5132 Merge branch '0.15' 2016-06-08 11:56:24 -07:00
Girish Ramakrishnan 9a177d9e46 fix typo 2016-06-08 10:14:59 -07:00
Girish Ramakrishnan 6f1df9980d minor wording changes 2016-06-08 09:58:30 -07:00
Girish Ramakrishnan 0c9d331f47 more changes 2016-06-08 08:46:18 -07:00
Girish Ramakrishnan f9db24e162 Fix autoupdate detection logic
We should be comparing existing manifest ports with new manifest ports.
The user chosen bindings itself doesn't matter.
2016-06-08 08:45:40 -07:00
Johannes Zellner 385bf3561b Remove unused function in platform.js 2016-06-08 15:06:03 +02:00
Johannes Zellner 4304f20fe0 Fix some log output 2016-06-08 15:05:43 +02:00
Johannes Zellner 509083265f fix setup_infra.sh to accomodate ifconfig differences 2016-06-08 15:01:28 +02:00
Johannes Zellner 1b9dbd06c8 Fix typo 2016-06-08 14:55:14 +02:00
Johannes Zellner d6482414bb Fixup the clientdb tests 2016-06-08 14:49:54 +02:00
Johannes Zellner 194b9b35bd We now have 3 built-in api clients 2016-06-08 14:48:03 +02:00
Johannes Zellner 6b9acb4722 Preserve the built-in clients on db clean 2016-06-08 14:47:21 +02:00
Johannes Zellner 08c3cb9376 Insert the default auth clients for tests 2016-06-08 14:37:41 +02:00
Johannes Zellner 79631ba996 Provide a fallback for the redirect uri 2016-06-08 14:30:42 +02:00
Johannes Zellner 4776a005a5 Remove redundant client TYPE_*s 2016-06-08 14:09:06 +02:00
Johannes Zellner e954df2120 Issue developer tokens with cid-cli 2016-06-08 13:39:31 +02:00
Johannes Zellner 526a62a20e Fix the async iterator 2016-06-08 13:35:32 +02:00
Johannes Zellner e2432d002f Count activeTokens correctly 2016-06-08 13:31:17 +02:00
Johannes Zellner 6e4d6d1099 Do not show application token listing if there is nothing to show 2016-06-08 13:03:59 +02:00
Johannes Zellner fc2d1d61d7 Also logout the webadmin session 2016-06-08 13:03:21 +02:00
Johannes Zellner 4c4ae08b44 Allow users to see issued tokens and revoke them all in one 2016-06-08 12:56:48 +02:00
Johannes Zellner 401c0e1b44 Special handling for better ui for sdk tokens 2016-06-08 11:55:40 +02:00
Johannes Zellner e431bd6040 Fix typo 2016-06-08 11:36:01 +02:00
Johannes Zellner a69cd204d6 Handle sdk and cli clients just like the webadmin 2016-06-08 11:33:08 +02:00
Johannes Zellner 3c3de6205e Add test case for blocking cid-webadmin deletion 2016-06-08 11:27:10 +02:00
Johannes Zellner 16444f775d Prevent deletion of the built-in clients 2016-06-08 11:24:02 +02:00
Johannes Zellner 2676658b5d Add auth client cid-sdk and cid-cli 2016-06-08 11:20:06 +02:00
Girish Ramakrishnan fbb8a842c1 do not require password 2016-06-08 00:07:41 -07:00
Girish Ramakrishnan 62b586e8dd fix require path 2016-06-07 20:57:39 -07:00
Girish Ramakrishnan 313d98ef70 add a route to check for updates quickly 2016-06-07 20:24:41 -07:00
Girish Ramakrishnan 06448f146d fix typo 2016-06-07 20:09:53 -07:00
Girish Ramakrishnan 064d950f87 add new tests for field validation 2016-06-07 16:00:02 -07:00
Girish Ramakrishnan 3236ce9cd6 check if both are null 2016-06-07 15:36:45 -07:00
Johannes Zellner f74b22645f Token view is now admin only 2016-06-07 22:56:27 +02:00
Johannes Zellner 3540f2c197 Move token management to separate view for admins only 2016-06-07 22:54:53 +02:00
Girish Ramakrishnan 3231fe7874 use user.get which already set admin flag 2016-06-07 10:03:08 -07:00
Girish Ramakrishnan dc8fd2eab3 do not use userdb directly 2016-06-07 10:01:14 -07:00
Girish Ramakrishnan 3ae388602c fix wording 2016-06-07 09:27:42 -07:00
Johannes Zellner 733187f3c4 Also show redirectURI for developers to use 2016-06-07 16:21:33 +02:00
Johannes Zellner 02d2a7058e Remove whitespace in scope input 2016-06-07 16:15:53 +02:00
Johannes Zellner 25003bcf40 Add placeholder text in scope input 2016-06-07 16:12:56 +02:00
Johannes Zellner 234caa60eb Add form validation for scopes 2016-06-07 16:10:33 +02:00
Johannes Zellner a0227b6043 Remove now unused localhost test client
We can now simply use the regular APIs to do local development against a Cloudron
2016-06-07 16:03:50 +02:00
Johannes Zellner 46ac6c4918 Offset the footer in the console views 2016-06-07 15:58:53 +02:00
Johannes Zellner 4afde79297 Fix error message 2016-06-07 15:56:22 +02:00
Johannes Zellner 17d48f3fce Specify the expiration on the client. Currently this is 100 years.
I am not sure if this is the best approach, or if we should introduce a magic value instead.
2016-06-07 15:54:32 +02:00
Johannes Zellner facdabcc8d Mention the token expiration in the ui 2016-06-07 15:54:09 +02:00
Johannes Zellner 691803f10b Allow optional expiresAt to be set on token creation 2016-06-07 15:47:13 +02:00
Johannes Zellner 8144c6d086 Patch up the token delete button with the route 2016-06-07 15:38:42 +02:00
Johannes Zellner 290ab6cc7d Fix typo 2016-06-07 15:38:30 +02:00
Johannes Zellner 8e5af17e5d Add route to delete a single token 2016-06-07 15:34:27 +02:00
Johannes Zellner d9d94faf75 Refresh the client on token actions 2016-06-07 15:15:56 +02:00
Johannes Zellner 0201ab19e4 Pass in the client object 2016-06-07 14:47:56 +02:00
Johannes Zellner 721fe74f3c The route creates the subobject 2016-06-07 14:47:47 +02:00
Johannes Zellner 96eeb247a1 Add rest api to create a new token for a client 2016-06-07 14:29:37 +02:00
Johannes Zellner 6261231593 add ui for token generation 2016-06-07 14:19:20 +02:00
Johannes Zellner d62d2b17fe Select tokens and secrets with single click 2016-06-07 14:10:20 +02:00
Johannes Zellner 89cef4f050 Show tokens for admins 2016-06-07 14:07:41 +02:00
Johannes Zellner 8602e033c5 Only show active clients for non-admins 2016-06-07 13:46:45 +02:00
Johannes Zellner 3598d89b12 ?all is gone in clients route 2016-06-07 13:17:02 +02:00
Johannes Zellner ffd552583c Patch up the client remove ui 2016-06-07 13:12:53 +02:00
Johannes Zellner 9eabc9d266 Fixup the wording 2016-06-07 12:50:52 +02:00
Johannes Zellner edf8cd736e Add modal for client removal 2016-06-07 12:48:12 +02:00
Johannes Zellner c5ebe2c2bf Fixup the panel padding 2016-06-07 12:48:00 +02:00
Johannes Zellner 5d0ccc0dd7 Show correct token count 2016-06-07 12:37:18 +02:00
Johannes Zellner 4147455654 Fetch tokens for each client separately 2016-06-07 12:37:04 +02:00
Johannes Zellner f3436a99a2 Fixup the client list details 2016-06-07 12:28:33 +02:00
Johannes Zellner 70d569e2e8 List all oauth clients in webadmin 2016-06-07 12:26:14 +02:00
Johannes Zellner 684625fbaf Remove unused clients.getAllWithDetailsByUserId() 2016-06-07 12:25:48 +02:00
Johannes Zellner c8b9ae542c Simply return oauth clients instead of join with tokendb 2016-06-07 12:15:25 +02:00
Johannes Zellner af29c1ba86 Handle external api client requests separately 2016-06-07 12:00:18 +02:00
Johannes Zellner 207e81345f Log event for external login 2016-06-07 11:59:54 +02:00
Johannes Zellner d880731351 Support ?all query param for oauth clients get route 2016-06-07 11:18:30 +02:00
Johannes Zellner e603cfe96e Add clients.getAllWithDetails() 2016-06-07 11:17:29 +02:00
Johannes Zellner 5b93a2870f Add clientdb.getAllWithTokenCount() 2016-06-07 11:17:14 +02:00
Johannes Zellner 1214300800 Consistent code styling 2016-06-07 11:08:35 +02:00
Johannes Zellner 8159334cbf Avoid more inlining 2016-06-07 10:55:36 +02:00
Johannes Zellner 78135c807a Be consistent in client.js add -> create 2016-06-07 10:49:11 +02:00
Johannes Zellner bfa33e4d8e Fixup a linter issue 2016-06-07 10:48:36 +02:00
Johannes Zellner 8b23174769 add client.addOAuthClient() 2016-06-07 10:45:50 +02:00
Johannes Zellner a078c94b97 Ensure autofocus is handled 2016-06-07 10:43:33 +02:00
Johannes Zellner c86392cd60 Add modal dialog to create API clients 2016-06-07 10:42:54 +02:00
Johannes Zellner f0e9256d46 Avoid style inlining 2016-06-07 10:10:58 +02:00
Girish Ramakrishnan 0cd4e4f03a update now takes appStoreId 2016-06-04 20:51:17 -07:00
Girish Ramakrishnan 1766da9174 update code path now takes appStoreId 2016-06-04 20:05:29 -07:00
Girish Ramakrishnan dbdcf1ec27 pass data object to update 2016-06-04 19:12:36 -07:00
Girish Ramakrishnan c916ea2589 fix style 2016-06-04 18:56:53 -07:00
Girish Ramakrishnan 5540b5f545 remove unused require 2016-06-04 18:55:31 -07:00
Girish Ramakrishnan 1e38190e68 setting falsy values for cert/key removes it 2016-06-04 18:30:05 -07:00
Girish Ramakrishnan 8f3553090f make args optional in configure 2016-06-04 18:07:06 -07:00
Girish Ramakrishnan cc0f5a1f03 fix configure arg insanity 2016-06-04 16:32:27 -07:00
Girish Ramakrishnan a1c531d2a8 better type checking in configure and make accessRestriction optional 2016-06-04 16:27:50 -07:00
Girish Ramakrishnan 57cb3b04d7 generate 2048-bit keys 2016-06-04 15:59:53 -07:00
Girish Ramakrishnan a49cf98a8d do not allow appId to be set
this is some legacy code
2016-06-04 13:40:43 -07:00
Girish Ramakrishnan da6cab8dd6 we return 400 now 2016-06-04 13:32:41 -07:00
Girish Ramakrishnan 3b7cfdd7db better type checking 2016-06-04 13:31:18 -07:00
Girish Ramakrishnan f9251c8b37 sending manifest is now redundant 2016-06-04 13:23:13 -07:00
Girish Ramakrishnan 4068ff5f21 add TODO note to validate accessRestriction 2016-06-04 13:20:10 -07:00
Girish Ramakrishnan ee073c91a3 return BAD_FIELD if app was not found 2016-06-04 13:15:38 -07:00
Girish Ramakrishnan 9e8742ca87 download manifest from appstore when appStoreId is provided 2016-06-04 01:07:43 -07:00
Girish Ramakrishnan 7f99fe2399 appStoreId has empty string default 2016-06-03 23:58:09 -07:00
Girish Ramakrishnan bfe8df35df toLowerCase in one place 2016-06-03 23:54:46 -07:00
Girish Ramakrishnan e2848d3e08 fix apps.install insane arg list 2016-06-03 23:35:55 -07:00
Girish Ramakrishnan bc823b4a75 make checkManifestConstraints return AppsError 2016-06-03 22:19:09 -07:00
Girish Ramakrishnan c24f780722 make validateAccessRestriction and validateMemory return AppsError 2016-06-03 22:16:55 -07:00
Girish Ramakrishnan 0d51ec9920 make validatePortBindings return AppsError 2016-06-03 22:15:02 -07:00
Girish Ramakrishnan e07e544029 make validateHostname return AppsError 2016-06-03 22:14:08 -07:00
Girish Ramakrishnan 5aff55c5ca typo when stashing altDomain 2016-06-03 19:50:01 -07:00
Girish Ramakrishnan 5ebc29746d fix failing tests 2016-06-03 19:14:16 -07:00
Girish Ramakrishnan 8fc44e6bc9 remove redundant checks 2016-06-03 19:08:47 -07:00
Girish Ramakrishnan 44f4872134 remove dead comments 2016-06-03 17:55:05 -07:00
Girish Ramakrishnan 49dd584a41 return expiresAt as ISO-string for API consistency 2016-06-03 10:11:09 -07:00
Girish Ramakrishnan 6d8f1f90d4 add note to change expires to TIMESTAMP type 2016-06-03 10:07:30 -07:00
Girish Ramakrishnan c1ded66c1a make download_url a post route 2016-06-03 09:23:15 -07:00
Johannes Zellner 4df49a82e5 Some clientdb.TYPE_ oversight in clients.js 2016-06-03 15:28:04 +02:00
Johannes Zellner 92e6ee9539 The clientSecret is now only ever created in the clients.js 2016-06-03 15:11:08 +02:00
Johannes Zellner 3ad2a2a5ca Fixup the unit tests 2016-06-03 15:07:44 +02:00
Johannes Zellner 226537de04 Move client TYPE_* to clients.js 2016-06-03 15:05:00 +02:00
Johannes Zellner 41b324eb2d Remove clientdb usage in addons.js 2016-06-03 14:56:45 +02:00
Johannes Zellner 1360729e97 Don't use clientdb directly from auth.js and apptask.js 2016-06-03 14:52:59 +02:00
Johannes Zellner 725e1debcc Provide getByAppIdAndType() by clients.js 2016-06-03 14:47:06 +02:00
Johannes Zellner 201efa70b7 use clients instead of clientdb in oauth2.js 2016-06-03 14:38:58 +02:00
Johannes Zellner c52d0369fa Provide better feedback on invalid scopes 2016-06-03 13:53:33 +02:00
Johannes Zellner b4dfad3aa3 Fixup the unit tests after removing PREFIX_USER 2016-06-03 13:09:26 +02:00
Johannes Zellner 7667cdc66d PREFIX_USER finally gone 2016-06-03 13:01:23 +02:00
Johannes Zellner 3a9a667890 Make all token grants without PREFIX_USER 2016-06-03 13:01:05 +02:00
Johannes Zellner 304cfed5a9 Result of password setting is now a plain token identifier 2016-06-03 13:00:07 +02:00
Johannes Zellner 778c583a52 Activation hands out a token without PREFIX_USER now 2016-06-03 12:59:13 +02:00
Johannes Zellner f988bb4d14 Do not use PREFIX_USER for token managment 2016-06-03 12:58:39 +02:00
Johannes Zellner 7057f1aaa2 All token identifiers are now plain user ids 2016-06-03 12:54:59 +02:00
Johannes Zellner e06f5f88b8 Remove the token types 2016-06-03 12:54:34 +02:00
Johannes Zellner 03cd3f0b6f Remove attached tokenType on req.user 2016-06-03 12:53:11 +02:00
Johannes Zellner 615f875169 Remove PREFIX_DEV for developer tokens 2016-06-03 12:52:10 +02:00
Johannes Zellner f27ba04a00 Add test case for developer tokens 2016-06-03 11:11:11 +02:00
Johannes Zellner 3e0006a327 Allow tokens with SCOPE_ROLE_SDK through without a password 2016-06-03 11:10:59 +02:00
Johannes Zellner 558ca42ae8 Issue developer tokens with SCOPE_ROLE_SDK 2016-06-03 11:10:22 +02:00
Johannes Zellner 9d8a803185 Handle scope roles in scope checks 2016-06-03 11:09:48 +02:00
Johannes Zellner 105047b0c4 Add SCOPE_ROLE_SDK 2016-06-03 11:08:35 +02:00
Johannes Zellner e335aa5dee Check for sdk token instead of token type DEV 2016-06-03 10:17:52 +02:00
Johannes Zellner 10163733db Separate the scope checking 2016-06-03 10:10:58 +02:00
Girish Ramakrishnan 251fad8514 add test for groupIds in listing api 2016-06-03 00:14:52 -07:00
Girish Ramakrishnan 036740f97b filter out correct fields in the route code 2016-06-03 00:04:17 -07:00
Girish Ramakrishnan f4958d936c return groupIds in get user route 2016-06-03 00:00:11 -07:00
Girish Ramakrishnan 80ca69a128 user.update does not need the user object 2016-06-02 23:53:06 -07:00
Girish Ramakrishnan 097d23c412 move logic to model code 2016-06-02 23:29:43 -07:00
Girish Ramakrishnan 13a1213b0d make group listing API return member userIds 2016-06-02 21:07:33 -07:00
Girish Ramakrishnan 76fe2bf531 add note to fix precision at some point 2016-06-02 19:43:23 -07:00
Girish Ramakrishnan 50c4e4c91e log event only after lock is acquired 2016-06-02 19:26:58 -07:00
Girish Ramakrishnan 46441d1814 cloudron.update is not exposed 2016-06-02 19:23:21 -07:00
Girish Ramakrishnan a4e73be834 pass auditSource for certificate renewal 2016-06-02 18:54:45 -07:00
Girish Ramakrishnan 6be0d0814d pass auditSource from cron.js 2016-06-02 18:51:50 -07:00
Girish Ramakrishnan e30d71921e pass auditSource for app autoupdater 2016-06-02 18:49:56 -07:00
Girish Ramakrishnan a49c78f32c make box autoupdate generate eventlog 2016-06-02 18:47:09 -07:00
Girish Ramakrishnan b077223e58 fix scope name 2016-06-02 17:49:54 -07:00
Girish Ramakrishnan d2864dfe56 rename root scope to cloudron scope (for lack of better scope name) 2016-06-02 16:51:14 -07:00
Girish Ramakrishnan 6d08af35a8 give developer token root scope 2016-06-02 15:58:40 -07:00
Girish Ramakrishnan 54f9d653f7 fix error messages 2016-06-02 14:41:21 -07:00
Girish Ramakrishnan 8d65f93fa4 return error.message 2016-06-02 14:40:29 -07:00
Girish Ramakrishnan 462440bb30 do not check for password in profile route
This is already checked by the verifyPassword middleware based on
the token type.

When using dev tokens, this check barfs for lack of password field
even when none is required.
2016-06-02 14:26:01 -07:00
Girish Ramakrishnan 65261dc4d5 add time_zone setter route 2016-06-02 13:54:07 -07:00
Girish Ramakrishnan 54ead09aac make the name API work
currently this only works for the main webadmin (and not for
nakeddomain, error etc) but that's fine.
2016-06-02 13:25:02 -07:00
Girish Ramakrishnan 28b3550214 use error.message 2016-06-02 13:00:23 -07:00
Girish Ramakrishnan e2e70da4c5 restrict length to 32 2016-06-02 12:51:49 -07:00
Johannes Zellner 7326ea27ca Only set username and displayName after successful update 2016-06-02 21:12:02 +02:00
Girish Ramakrishnan 1fe00f7f80 do not use verbs in resource url 2016-06-02 12:01:48 -07:00
Girish Ramakrishnan e9e9d6000d remove token check for user.update to work with dev tokens 2016-06-02 11:29:59 -07:00
Girish Ramakrishnan 6dccb3655f add no groups available message in edit user dialog 2016-06-02 10:55:34 -07:00
Girish Ramakrishnan c3113bd74d go back to step2 if activation fails 2016-06-02 10:40:06 -07:00
Girish Ramakrishnan e79119b72a 0.15.0 changes 2016-06-02 10:32:10 -07:00
Johannes Zellner 086cfdc1e6 Disabled form fields are not POSTed
I did not know about that fact, one has to use readonly
2016-06-02 16:12:32 +02:00
Johannes Zellner 1f091d3b4b We have to let angular know 2016-06-02 16:06:15 +02:00
Johannes Zellner 892fa4b2ec We still require the username to be sent always 2016-06-02 16:01:25 +02:00
Johannes Zellner a87b4b207c Adhere to already set username in user setup view 2016-06-02 15:47:58 +02:00
Johannes Zellner bdd14022d6 Report user conflict message all the way through the rest routes 2016-06-02 15:41:07 +02:00
Johannes Zellner 3d40cf03b1 Pass down the reason why the user conflicts 2016-06-02 15:39:21 +02:00
Johannes Zellner 594be7dbbd Allow the userdb code to distinguish between username or email duplicates 2016-06-02 15:34:27 +02:00
Johannes Zellner a52e2ffc23 Distinguish between username and email conflict 2016-06-02 15:19:35 +02:00
Johannes Zellner 8eeee712aa Remove unused require 2016-06-02 14:14:16 +02:00
Johannes Zellner 0f62faa198 All our tokens are now representing an user with a profile 2016-06-02 14:13:57 +02:00
Johannes Zellner bfd66cf309 Remove unused token PREFIX_APP 2016-06-02 14:07:41 +02:00
Johannes Zellner c2f7d61e34 Remove unused token TYPE_APP 2016-06-02 14:07:19 +02:00
Johannes Zellner d5d5e356ae Add error handling for invalid usernames 2016-06-02 13:52:44 +02:00
Johannes Zellner 531752cd43 Use placeholder for description in username and displayName fields 2016-06-02 13:52:33 +02:00
Johannes Zellner 9eac56578c Allow admins to set the username and displayName optionally on user creation 2016-06-02 13:32:45 +02:00
Johannes Zellner d06398dbfd Move webdav nginx fixes into app endpoint
Not sure if this will now still work with oauth proxy though.
2016-06-02 09:49:01 +02:00
Girish Ramakrishnan 60ce6b69ee profile updates must be POST 2016-06-02 00:31:41 -07:00
Girish Ramakrishnan 4fcc7fe99f updateUser is POST 2016-06-02 00:27:06 -07:00
Girish Ramakrishnan 82cd215ffa merge bad fields and pass error.message correctly in REST responses 2016-06-02 00:12:21 -07:00
Girish Ramakrishnan 1dcea84068 fix typo 2016-06-01 23:43:21 -07:00
Girish Ramakrishnan 4107252bfe developer mode is true by default 2016-06-01 23:13:12 -07:00
Girish Ramakrishnan 9cc6cb56f7 fix error message 2016-06-01 19:38:42 -07:00
Girish Ramakrishnan 48b99a4203 enable developer mode by default
also emphasize on the api aspect
2016-06-01 18:57:15 -07:00
Girish Ramakrishnan 824767adbb enabled -> isEnabled 2016-06-01 18:21:02 -07:00
Girish Ramakrishnan 3d84880d92 keep limits in sync with the nginx config 2016-06-01 18:09:23 -07:00
Girish Ramakrishnan dfa08469d6 set timeouts explicitly 2016-06-01 17:33:28 -07:00
Girish Ramakrishnan d798073d95 fix comment of default_server 2016-06-01 17:28:15 -07:00
Girish Ramakrishnan 41632b8c11 fix favicon of naked domain 2016-06-01 17:27:39 -07:00
Girish Ramakrishnan 6ccc46717e remove unused middleware 2016-06-01 16:59:51 -07:00
Girish Ramakrishnan 2495caf2eb remove unused redis module 2016-06-01 16:55:58 -07:00
Girish Ramakrishnan ae9c104a8b remove unused bytes module 2016-06-01 16:54:25 -07:00
Girish Ramakrishnan 683f371778 remove serve-favicon
favicon is served up as /api/v1/cloudron/avatar
2016-06-01 16:52:28 -07:00
Girish Ramakrishnan eb29bdd575 document keepalive_timeout 2016-06-01 16:51:52 -07:00
Girish Ramakrishnan b13de298bf Add some REST api tests 2016-06-01 16:33:18 -07:00
Johannes Zellner 47978436c2 Set Destination header for webdav in nginx proxy 2016-06-01 18:49:50 +02:00
Johannes Zellner 71b5cc4702 Fix invite mail wording 2016-06-01 18:45:31 +02:00
Girish Ramakrishnan 5a9e32d41a hide aliases field if no username is set 2016-06-01 06:33:29 -07:00
Girish Ramakrishnan b03e4db8d5 check for null username 2016-05-31 21:38:51 -07:00
Girish Ramakrishnan 663ff2410a user.update must become a post route 2016-05-31 11:51:56 -07:00
Girish Ramakrishnan f763759008 return empty groupIds 2016-05-31 11:49:59 -07:00
Girish Ramakrishnan 69aa11d6c6 send pretty json 2016-05-31 11:14:59 -07:00
Girish Ramakrishnan 65041743c5 update to connect-lastmile@0.1.0 2016-05-31 10:48:37 -07:00
Girish Ramakrishnan 76214d3d7a no variable named infra_version 2016-05-30 19:45:59 -07:00
Girish Ramakrishnan be83a967fc node require will not work without json extension 2016-05-30 17:03:56 -07:00
Girish Ramakrishnan 119e095710 actually change ownership 2016-05-30 15:51:52 -07:00
Girish Ramakrishnan 5df3a41988 INFRA_VERSION may not exist 2016-05-30 14:48:41 -07:00
Girish Ramakrishnan a34b611e20 make INFRA_VERSION writable by yellowtent user 2016-05-30 12:52:39 -07:00
Girish Ramakrishnan 75c1731443 do not add app mailboxes to database
a) we don't allow .app pattern in database for aliases and mailboxes
b) the addons already know about app names separately
2016-05-30 01:38:43 -07:00
Girish Ramakrishnan 9e36b7abf4 load addon vars for existing infra case 2016-05-30 01:06:41 -07:00
Girish Ramakrishnan b37226d4d1 fix ui issues 2016-05-30 00:07:58 -07:00
Girish Ramakrishnan 311efe5d10 test: add test for getting aliases 2016-05-29 23:23:03 -07:00
Girish Ramakrishnan ebdd6d8a31 add missing require 2016-05-29 23:15:55 -07:00
Girish Ramakrishnan 3ee9f70113 return alias info in mailbox response 2016-05-29 23:14:24 -07:00
Girish Ramakrishnan adfc069e16 allow user alias to be set for custom domains 2016-05-29 23:02:01 -07:00
Girish Ramakrishnan 31fd0d711a add setAliases 2016-05-29 22:45:48 -07:00
Girish Ramakrishnan a6a852cfae remove bogus debug 2016-05-29 22:01:21 -07:00
Girish Ramakrishnan e9b3e22e86 check version of existing infra 2016-05-29 21:58:04 -07:00
Girish Ramakrishnan 564d61bcf5 fix typo 2016-05-29 21:31:49 -07:00
Girish Ramakrishnan 5582ac7402 add some debugs 2016-05-29 21:28:55 -07:00
Girish Ramakrishnan a05b6ad78d delete mailbox on user delete 2016-05-29 21:02:51 -07:00
Girish Ramakrishnan ec71390d0b autocreate mailbox when username is available 2016-05-29 19:14:01 -07:00
Girish Ramakrishnan 68a3862ee5 add create and remove mailbox 2016-05-29 18:56:40 -07:00
Girish Ramakrishnan a9f70d8363 add mailbox search endpoint 2016-05-29 18:24:54 -07:00
Girish Ramakrishnan e91539d79a add a todo 2016-05-29 18:08:16 -07:00
Girish Ramakrishnan 5546bfbf0e add mailbox ldap auth point 2016-05-29 17:25:23 -07:00
Girish Ramakrishnan 803d47b426 refactor authenticate path into a middleware 2016-05-29 17:16:52 -07:00
Girish Ramakrishnan e4c0192243 rename to appUserBind since it is tailored for apps 2016-05-29 17:07:48 -07:00
Girish Ramakrishnan d5b5289e0c Add mailbox importer for existing users and apps
this should prevent conflicts of mailboxes from the get-go.
2016-05-28 02:07:43 -07:00
Girish Ramakrishnan 2909aad72a use async.series 2016-05-28 01:59:48 -07:00
Girish Ramakrishnan cafbb31e78 push aliases to mail container on startup 2016-05-28 01:53:25 -07:00
Girish Ramakrishnan 080128539c set/unset aliases on the mail container 2016-05-28 01:33:20 -07:00
Girish Ramakrishnan cf93a99a4e add a note about mailboxes 2016-05-27 22:28:56 -07:00
Girish Ramakrishnan ce927bfa22 alias
also remove id since it's not useful for mailbox case (not like
mailbox can be renamed and we need a fixed it)
2016-05-27 22:20:08 -07:00
Girish Ramakrishnan 6993a9c7e7 add more mailbox route test 2016-05-27 18:23:14 -07:00
Girish Ramakrishnan 84d04cce16 initial mailboxes route 2016-05-27 18:17:57 -07:00
Girish Ramakrishnan f735fd8172 add mailboxes tests 2016-05-27 17:43:25 -07:00
Girish Ramakrishnan 53e28db1d6 add note on accessRestriction 2016-05-27 11:10:36 -07:00
Girish Ramakrishnan 77457d1ea9 initial mailbox db and model code 2016-05-27 10:36:47 -07:00
Girish Ramakrishnan 161b7cf76b add mailboxes table 2016-05-26 21:08:20 -07:00
Girish Ramakrishnan 01b6defd24 mount mail container /run into data 2016-05-26 15:18:10 -07:00
Girish Ramakrishnan badc524ff2 '-' has special meaning haraka
so do '.app' instead
2016-05-26 10:58:30 -07:00
Girish Ramakrishnan b3f53099f0 allow only alpha numerals in username 2016-05-25 21:36:20 -07:00
Girish Ramakrishnan a28560cdc0 0.14.2 changes 2016-05-24 20:17:20 -07:00
Girish Ramakrishnan 4afdf50736 finally finally finally the tests are working 2016-05-24 20:04:37 -07:00
Girish Ramakrishnan 078e36f07f turns out we cannot use sudo since it asks for password 2016-05-24 19:59:47 -07:00
Girish Ramakrishnan 3b8e15a61c check for node in path 2016-05-24 18:32:15 -07:00
Girish Ramakrishnan 67682c5d27 check for test image 2016-05-24 17:44:19 -07:00
Girish Ramakrishnan 48e3b8ebf9 provide a dummy config for tests 2016-05-24 17:36:54 -07:00
Girish Ramakrishnan 2072dedf66 bump mail addon version 2016-05-24 16:46:54 -07:00
Girish Ramakrishnan 51f43ecc27 use infra_version.js in checkInstall 2016-05-24 16:46:27 -07:00
Girish Ramakrishnan 2347a7ced2 admin email is a platform property 2016-05-24 16:36:56 -07:00
Girish Ramakrishnan b2cadaf95c load vars files after the platform is created 2016-05-24 16:28:59 -07:00
Girish Ramakrishnan 957f787701 setup mail addon root credentials 2016-05-24 16:18:21 -07:00
Girish Ramakrishnan ad48067bb2 setup_infra now uses infra_version.js
INFRA_VERSION is now removed. Note that DATA_DIR/INFRA_VERSION
still exists.
2016-05-24 16:16:03 -07:00
Girish Ramakrishnan 12b6c46558 use infra_version.js in splashpage 2016-05-24 13:31:45 -07:00
Girish Ramakrishnan b4ba17c599 use the constant 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 7fb28662c1 remove old images in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 4845db538a use infra_version.js in platform.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan 8429985253 use infra_version.js in addons.js 2016-05-24 13:23:41 -07:00
Girish Ramakrishnan aff9ff47bc use infra_version.js in baseimage script 2016-05-24 13:23:38 -07:00
Girish Ramakrishnan 9b3077eca3 add infra_version.js 2016-05-24 13:02:38 -07:00
Girish Ramakrishnan 39396cb3ab call callback if provided 2016-05-24 11:39:05 -07:00
Girish Ramakrishnan 364f0ead51 debug out the cmd 2016-05-24 11:26:11 -07:00
Girish Ramakrishnan 5ac1d5575c platform.js: minor refactor 2016-05-24 10:58:18 -07:00
Girish Ramakrishnan e5a030baff move platform cleanup bits to javascript 2016-05-24 10:52:55 -07:00
Girish Ramakrishnan a100837e69 Add helpers to restore/reconfigure all apps 2016-05-24 10:44:45 -07:00
Girish Ramakrishnan ffacf17a42 add paths.INFRA_VERSION_FILE 2016-05-24 10:26:08 -07:00
Girish Ramakrishnan f5d37b6443 add ini module 2016-05-24 10:25:33 -07:00
Girish Ramakrishnan d71d09c1ba Add shell.execSync 2016-05-24 10:22:39 -07:00
Girish Ramakrishnan c1a2444dfa move container creation to platform.js 2016-05-24 09:40:26 -07:00
Girish Ramakrishnan ef40aae3ba set adminEmail to no-reply@localhost for tests 2016-05-24 00:54:38 -07:00
Girish Ramakrishnan 9570086c87 add config.smtpPort 2016-05-24 00:53:42 -07:00
Girish Ramakrishnan 57a823a698 make tests work 2016-05-24 00:44:01 -07:00
Girish Ramakrishnan ec0ee07b17 test: email works now 2016-05-23 23:17:38 -07:00
Girish Ramakrishnan 3d7545133e wait 30 secs in the beginning 2016-05-23 23:16:02 -07:00
Girish Ramakrishnan bcc752469a remove containers after the test 2016-05-23 22:47:40 -07:00
Girish Ramakrishnan da85f4c096 stop ldap server in test 2016-05-23 21:59:06 -07:00
Girish Ramakrishnan 3b740a5651 make tests work 2016-05-23 20:41:00 -07:00
Girish Ramakrishnan 7eb202f19a test: use the test-app instead of duplicating the checks in the tests 2016-05-23 20:17:11 -07:00
Girish Ramakrishnan 8dbd4c8527 use 1024 bit keys
Stacked error: error:04075070:rsa routines:RSA_sign:digest too big for rsa key
2016-05-23 19:31:57 -07:00
Girish Ramakrishnan 88f2ce554d bump to mail 0.13.1 with the auth fix 2016-05-23 18:57:59 -07:00
Girish Ramakrishnan 57888659a6 Update test app version 2016-05-23 18:31:12 -07:00
Girish Ramakrishnan ebdefa7f18 test: oauth addon 2016-05-23 17:34:25 -07:00
Girish Ramakrishnan 569150f602 tests: create a self-signed cert 2016-05-23 16:40:18 -07:00
Girish Ramakrishnan 6ccb806628 make apps-test pass 2016-05-23 16:31:02 -07:00
Girish Ramakrishnan ae807b28b6 test: let server start the infra
otherwise, deps like dkim keys need to be setup in tests as well
2016-05-23 15:53:51 -07:00
Girish Ramakrishnan 00726b01e2 pass -no-run-if-empty instead 2016-05-23 15:50:36 -07:00
Girish Ramakrishnan f5b777ab33 add route tests for username 2016-05-23 15:00:21 -07:00
Girish Ramakrishnan d84e584222 add some username tests 2016-05-23 14:56:09 -07:00
Girish Ramakrishnan 31e452e1cc test: mixed case reserved name 2016-05-23 14:52:29 -07:00
Girish Ramakrishnan e015b9bd7a 0.14.1 changes 2016-05-23 12:29:49 -07:00
Girish Ramakrishnan 10e0cbcebc do not set allowHalfOpen (otherwise we have to end socket ourself) 2016-05-23 10:50:04 -07:00
Girish Ramakrishnan 2768c3a336 acme: configure prod based on caas or acme 2016-05-23 09:48:17 -07:00
Girish Ramakrishnan 37512c4cac Wrap the stdin stream to indicate EOF
The docker exec protocol supports half-closing to signal that the stdin
is finished. The CLI tool tried to do this by closing it's half of the
socket when the stdin finished. Unfortunately, this does not work because
nginx immediately terminates a half-close :/ Node itself has no problem.

http://mailman.nginx.org/pipermail/nginx/2008-September/007388.html
seems to support the hypothesis. Basically, for HTTP and websockets
there is no notion of half-close.

Websocket protocol itself has no half-close as well:
http://www.lenholgate.com/blog/2011/07/websockets---i-miss-the-tcp-half-close.html
http://doc.akka.io/docs/akka/2.4.5/scala/http/client-side/websocket-support.html

The fix is to implement our own protocol that wrap stdin. We put a length
header for every payload. When we hit EOF, the length is set to 0. The server
sees this 0 length header and closes the exec container socket.
2016-05-22 22:27:49 -07:00
Girish Ramakrishnan 0aaaa866e4 Add a whole bunch of magic for docker.exec to work 2016-05-22 00:27:32 -07:00
Girish Ramakrishnan 53cb7fe687 debug out cmd 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan da42f2f00c fix boolean logic 2016-05-19 15:54:35 -07:00
Girish Ramakrishnan 27d2daae93 leave a note in nginx config 2016-05-19 12:27:54 -07:00
Girish Ramakrishnan 42cc8249f8 reserve usernames with -app in them 2016-05-18 21:45:02 -07:00
Girish Ramakrishnan de055492ef set username restriction to 2 chars 2016-05-18 11:05:45 -07:00
Girish Ramakrishnan efa3ccaffe fix crash because of missing error handling 2016-05-18 10:00:32 -07:00
Girish Ramakrishnan 32e238818a make script more robust 2016-05-17 18:16:18 -07:00
Girish Ramakrishnan 2c1083d58b log error if the curl parsing fails 2016-05-17 17:15:10 -07:00
Girish Ramakrishnan 517b967fe9 enable sieve 2016-05-17 14:04:57 -07:00
Girish Ramakrishnan 3c4ca8e9c8 reserve more usernames 2016-05-17 12:47:10 -07:00
Girish Ramakrishnan 4ec043836b 0.14.0 changes 2016-05-17 09:36:28 -07:00
Girish Ramakrishnan 7ec93b733b setup restore paths for recvmail and email addon 2016-05-17 09:27:59 -07:00
Girish Ramakrishnan a81262afb5 remove unused var 2016-05-17 08:47:57 -07:00
Girish Ramakrishnan 266603bb19 Do not barf on rmi 2016-05-16 16:56:17 -07:00
Girish Ramakrishnan 6dcecaaf55 log the ldap source 2016-05-16 14:31:57 -07:00
Girish Ramakrishnan 099eb2bca4 use port 2525 2016-05-16 12:52:36 -07:00
Girish Ramakrishnan b92ed8d079 cn can also be the cloudron email
cn can be:
1. username
2. username@fqdn
3. user@personalemail.com
2016-05-16 12:21:15 -07:00
Girish Ramakrishnan 0838ce4ef8 fix casing 2016-05-16 12:13:23 -07:00
Girish Ramakrishnan cc8767274a Bind 587 with 2525 as well
MSA (587) and MTA (25) are the same protocol. Only difference is
that 587 does relaying with AUTH. Haraka has been configured to
make this distinction.
2016-05-16 08:32:30 -07:00
Girish Ramakrishnan dfaed79e31 fix mail container name 2016-05-16 08:18:33 -07:00
Girish Ramakrishnan 9dc1a95992 SENDIMAGE -> IMAGE 2016-05-15 21:51:04 -07:00
Girish Ramakrishnan 5be05529c2 remove unused ldap ou 2016-05-15 21:25:56 -07:00
Girish Ramakrishnan 6ff7786f04 use the send addon service api 2016-05-15 21:23:44 -07:00
Girish Ramakrishnan a833b65ef3 add mail container link once 2016-05-15 21:18:43 -07:00
Girish Ramakrishnan 83a252bd20 there is only mail container now 2016-05-15 21:15:53 -07:00
Girish Ramakrishnan 7ef3805dbc docker data in test dir requires sudo 2016-05-13 22:38:36 -07:00
Girish Ramakrishnan e5d906a065 create fake cert/key for tests to pass 2016-05-13 22:35:13 -07:00
Girish Ramakrishnan b5e4e9fed6 bump postgresql 2016-05-13 22:13:58 -07:00
Girish Ramakrishnan 3ccb72f891 tests finally work 2016-05-13 22:05:05 -07:00
Girish Ramakrishnan 45cd4ba349 fix name 2016-05-13 22:03:40 -07:00
Girish Ramakrishnan 5b9b21c469 create recvmail link 2016-05-13 22:03:34 -07:00
Girish Ramakrishnan ed55ad1c6f change image name 2016-05-13 21:34:04 -07:00
Girish Ramakrishnan 560f460a32 rename to sendmail 2016-05-13 20:48:31 -07:00
Girish Ramakrishnan aa116ce58c fix tests 2016-05-13 20:21:09 -07:00
Girish Ramakrishnan 3f0e2024e4 pass db name and password for tests 2016-05-13 19:35:20 -07:00
Girish Ramakrishnan d9c5b2b642 setup and teardown recvmail addon 2016-05-13 18:58:48 -07:00
Girish Ramakrishnan 5322ed054d reserve 4190 for sieve 2016-05-13 18:48:05 -07:00
Girish Ramakrishnan 39c4954371 remove isIncomingMailEnabled. always enable for now
also, custom domain === we will take over domain completely (setup
mx and all)
2016-05-13 18:44:08 -07:00
Girish Ramakrishnan 78ad49bd74 hack for settings test 2016-05-13 18:27:00 -07:00
Girish Ramakrishnan f56c960b92 use latest manifestformat (for recvmail, email) 2016-05-13 17:59:11 -07:00
Girish Ramakrishnan 8e077660c4 start_addons is redundant 2016-05-13 17:57:56 -07:00
Girish Ramakrishnan 1b8b4900a2 parametrize data_dir for tests 2016-05-13 17:57:56 -07:00
Girish Ramakrishnan 27ddcb9758 use the internal hostnames for email addon
The public ones are for the user to configure their MUA. This
things can have ssl disabled safely as well.
2016-05-13 08:28:13 -07:00
Girish Ramakrishnan fdb951c9e5 set MAIL_DOMAIN in email addon as well 2016-05-12 23:34:35 -07:00
Girish Ramakrishnan 0f2037513b remove recvmail bind 2016-05-12 21:48:42 -07:00
Girish Ramakrishnan 9da4e038bd all lower case 2016-05-12 18:54:13 -07:00
Girish Ramakrishnan ae3e0177bb make script more robust 2016-05-12 18:16:26 -07:00
Girish Ramakrishnan 0751974624 Add a hack to test addon patching 2016-05-12 16:41:58 -07:00
Girish Ramakrishnan b8242c82d6 create bind point for recvmail 2016-05-12 14:33:02 -07:00
Girish Ramakrishnan 442c02fa1b set mailAlternateAddress to username@fqdn
This is mostly to keep haraka's rcpt_to.ldap happy. That plugin
could do with some love.
2016-05-12 14:32:15 -07:00
Girish Ramakrishnan d5306052bb refactor code for readability 2016-05-12 13:36:53 -07:00
Girish Ramakrishnan 8543dbe3be create a new ou for addons 2016-05-12 13:20:57 -07:00
Girish Ramakrishnan bf42b735d1 fix the smtp port 2016-05-12 09:11:29 -07:00
Girish Ramakrishnan a2ba3989d0 use manifestformat 2.4.0 2016-05-12 08:57:12 -07:00
Girish Ramakrishnan 6f36d79358 implement email addon 2016-05-12 08:54:59 -07:00
Girish Ramakrishnan 1da24564b3 reserve postman subdomain 2016-05-11 15:04:22 -07:00
Girish Ramakrishnan da61d5c0f1 add ou=recvmail for dovecot 2016-05-11 14:26:34 -07:00
Girish Ramakrishnan 79da7b31c7 use latest base image 2016-05-11 13:14:11 -07:00
Girish Ramakrishnan 631b238b63 use MAIL_LOCATION for mx record 2016-05-11 09:59:12 -07:00
Girish Ramakrishnan ff5ca617b1 use ADMIN_LOCATION 2016-05-11 09:58:31 -07:00
Girish Ramakrishnan e16125c67e set MAIL_DOMAIN and MAIL_SERVER_NAME 2016-05-11 09:49:20 -07:00
Girish Ramakrishnan 646ba096c3 start recvmail addon in setup_infra 2016-05-11 08:55:51 -07:00
Girish Ramakrishnan 8be3b4c281 add mx records 2016-05-11 08:50:33 -07:00
Girish Ramakrishnan 5afff5eecc open 25 (inbound smtp) and 587 (inbound submission) 2016-05-11 08:48:50 -07:00
Girish Ramakrishnan 84206738e1 open port 993 (imap) 2016-05-11 08:47:59 -07:00
Girish Ramakrishnan 8b2e4ce700 do another infra bump for existing cloudrons 2016-05-10 16:53:13 -07:00
Girish Ramakrishnan 776f184dbc wait randomly instead 2016-05-10 16:51:20 -07:00
Girish Ramakrishnan a54466f8c2 Fix apptask error because of multiple collectd restarts
Everyone gets in a rush to restart collectd and apptask update/restore
fails during infra updates
2016-05-10 09:52:55 -07:00
Girish Ramakrishnan f36641b443 better error message 2016-05-10 09:50:57 -07:00
Girish Ramakrishnan 36eb107b83 0.13.4 changes 2016-05-10 09:16:16 -07:00
Girish Ramakrishnan fa16ae9a0c Use mail container 0.12.0 that contains the restart fix 2016-05-10 09:05:06 -07:00
Girish Ramakrishnan 517b36b3f0 fix typo 2016-05-09 23:58:35 -07:00
Girish Ramakrishnan 83a28afc8f addons are started by box code now 2016-05-09 23:56:02 -07:00
Girish Ramakrishnan e76c7de259 bump test app version 2016-05-09 23:40:59 -07:00
Girish Ramakrishnan fcda4a771c play around with some text 2016-05-09 18:47:11 -07:00
Girish Ramakrishnan 76d8f16e22 0.13.3 changes 2016-05-08 01:17:03 -07:00
Girish Ramakrishnan 1b3cd1f373 remove tls since server does not offer it anymore 2016-05-08 01:15:24 -07:00
Girish Ramakrishnan 62b020e96d add note 2016-05-07 02:34:52 -07:00
Girish Ramakrishnan bc78f4a6d8 fix user to match adminEmail 2016-05-07 01:32:14 -07:00
Girish Ramakrishnan a8e458e935 Load certs into etc 2016-05-06 21:46:22 -07:00
Johannes Zellner e4747ef50c Rework the tutorial 2016-05-06 21:32:34 +02:00
Johannes Zellner 0d6637de27 Avoid circular dependencies with apps and certificates 2016-05-06 18:44:37 +02:00
Johannes Zellner 28e513a434 Fix eventlog tests 2016-05-06 18:05:28 +02:00
Girish Ramakrishnan e73174685b add note on settings route 2016-05-06 08:42:27 -07:00
Johannes Zellner 3af95508f5 eventlog getAllPaged is now getByQueryPaged 2016-05-06 17:27:52 +02:00
Johannes Zellner 4fa8ab596b Show busy state in eventlogs 2016-05-06 17:23:39 +02:00
Johannes Zellner 54c9bb7409 Add filter bar for event log view 2016-05-06 17:18:47 +02:00
Johannes Zellner 4c7dc5056d Add more query options for eventlog api 2016-05-06 16:49:17 +02:00
Johannes Zellner e986a67d39 Fixup all the unit tests 2016-05-06 15:16:22 +02:00
Johannes Zellner da8de173a6 Remove appdb.getBySubdomain() 2016-05-06 14:52:33 +02:00
Johannes Zellner cbc906f8d1 Remove apps.getBySubdomain() 2016-05-06 14:52:06 +02:00
Johannes Zellner c7958f8e1d Remove unused /api/v1/subdomains/:subdomain 2016-05-06 14:51:02 +02:00
Johannes Zellner b88ee8143a Rename welcome -> tutorial 2016-05-06 14:41:20 +02:00
Johannes Zellner e413f7ba9b Handle tutorial walkthrough 2016-05-06 14:38:17 +02:00
Johannes Zellner 7e1055ae44 Show tutorial for admins 2016-05-06 14:23:21 +02:00
Johannes Zellner c61ce40362 Add /api/v1/profile/tutorial route tests 2016-05-06 14:06:54 +02:00
Johannes Zellner e48156dceb postprocess showTutorial to ensure we deal with a boolean 2016-05-06 14:05:47 +02:00
Johannes Zellner f3811e3df9 Remove all unused vars in profile test 2016-05-06 14:03:24 +02:00
Johannes Zellner 0d40b1b80d Fix the test wording in profile password change tests 2016-05-06 14:02:21 +02:00
Johannes Zellner 8b92c8f7ae Remove unused checkMails() in profile tests 2016-05-06 13:57:46 +02:00
Johannes Zellner d41eb81b3d Add new profile/ route to set the showTutorial field 2016-05-06 13:56:40 +02:00
Johannes Zellner 3adf91afed Add setShowTutorial() api to users.js 2016-05-06 13:56:26 +02:00
Johannes Zellner 18f05de8ae use users.showTutorial field in userdb 2016-05-06 13:56:05 +02:00
Johannes Zellner b0f4396389 Add showTutorial field to users table 2016-05-06 13:55:03 +02:00
Johannes Zellner bf99475dbd Introduce basic welcome tutorial flow 2016-05-06 13:06:12 +02:00
Girish Ramakrishnan d50fa70f47 pass -out 2016-05-05 21:26:13 -07:00
Girish Ramakrishnan 0e655cadb0 generate dkim keys before dns setup
Two things require DKIM keys
1. the mail addon
2. the DNS TXT record
2016-05-05 21:15:10 -07:00
Girish Ramakrishnan 496e1c3dc1 fix path to INFRA_VERSION 2016-05-05 18:37:17 -07:00
Girish Ramakrishnan 325252699e set MAIL_FROM more smartly 2016-05-05 18:33:22 -07:00
Girish Ramakrishnan 2d43e22285 fix typo 2016-05-05 15:26:32 -07:00
Girish Ramakrishnan 9e673c3890 supply a bogus username/password 2016-05-05 15:24:52 -07:00
Girish Ramakrishnan c3c18e8a4b reserve more ports 2016-05-05 15:00:07 -07:00
Girish Ramakrishnan cb1bd58cb9 do not export submission port just yet 2016-05-05 14:53:07 -07:00
Girish Ramakrishnan 0bdff14c9f r -> ro 2016-05-05 13:54:57 -07:00
Girish Ramakrishnan c4ae9526af look for fallback cert in nginx cert dir 2016-05-05 13:52:08 -07:00
Girish Ramakrishnan 8d79ac9ae0 provide tls cert and key to mail server
haraka requires tls certs for:
1. supporting AUTH
2. port 587 support (MSA)

currently, we just reuse the cert for the admin domain. Otherwise,
we have to setup dns etc to get a new cert. While doable, its' not
necessary right now.
2016-05-05 13:18:17 -07:00
Girish Ramakrishnan 7d4ed5bafc Revert "x"
This reverts commit a1554b9cc1642037e9fbd0d261722c908f499aab.

committed by mistake
2016-05-05 12:57:04 -07:00
Johannes Zellner 85db8f398b Ensure we require an admin for settings routes
The cloudron name and avatar already have public routes.
2016-05-05 12:48:21 +02:00
Girish Ramakrishnan 636b71ce6f Add MAIL_FROM env 2016-05-05 00:28:08 -07:00
Girish Ramakrishnan b46008f0b1 add sendmail ou bind
this will be used by haraka to authenticate the apps
2016-05-05 00:26:43 -07:00
Girish Ramakrishnan 9d9bd42cd2 x 2016-05-05 00:09:16 -07:00
Girish Ramakrishnan 0f6a2a42f2 use latest mysql image 2016-05-04 23:09:30 -07:00
Girish Ramakrishnan fc8bf82993 Add getters for fallback and admin cert 2016-05-04 17:37:21 -07:00
Girish Ramakrishnan e56192913d reserved smtp and imap locations 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 2d08ce441f rename arg_fqdn to fqdn 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 87497c2047 pass fqdn as arg 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 32a0bf6fd2 add --check arg to setup_infra.sh 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 291e625785 skip infra setup for tests (the test does it itself) 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 6bcfd33e10 boot2docker is dead 2016-05-04 15:54:21 -07:00
Girish Ramakrishnan b4c15b1719 Let the box code initialize the infrastructure
This is done because:
1. The box code can install certs for addons (like mail addon) when
   required.

2. The box code initialize/teardown addons on demand. This is not planned
   currently.
2016-05-04 15:54:21 -07:00
Girish Ramakrishnan 920626192c fix error message 2016-05-04 09:28:50 -07:00
Johannes Zellner 778371b818 Only send out mails if the admin group has changed 2016-05-04 13:55:14 +02:00
Girish Ramakrishnan d2a1cea1e8 0.13.2 changes 2016-05-03 23:48:45 -07:00
Girish Ramakrishnan 5683cefe89 provide auditSource when autoupdating app 2016-05-03 23:36:27 -07:00
Girish Ramakrishnan 126d64ffa8 move backup event in backupBoxAndApps
the updater uses this code route
2016-05-03 18:36:52 -07:00
Girish Ramakrishnan 7262eb208f add route to get the timezone 2016-05-03 12:10:21 -07:00
Girish Ramakrishnan f57f8f5e58 fix typo 2016-05-03 12:09:58 -07:00
Girish Ramakrishnan 00ad1308aa handle empty time_zone 2016-05-03 12:09:22 -07:00
Girish Ramakrishnan 2cc37a9c31 0.13.1 changes 2016-05-03 11:50:52 -07:00
Girish Ramakrishnan 13b0093f20 telize has been discontinued. use freegeoip 2016-05-03 11:50:31 -07:00
Girish Ramakrishnan 5617baea50 eventlog tests 2016-05-02 14:54:25 -07:00
Girish Ramakrishnan da79e4f229 only admin can view activity logs 2016-05-02 14:54:20 -07:00
Girish Ramakrishnan 6c1bd522e6 add pagination 2016-05-02 11:50:15 -07:00
Girish Ramakrishnan 7fca9decc1 fixes to eventlog 2016-05-02 11:07:55 -07:00
Girish Ramakrishnan 7cf08b6a4d add start event
this signals that an update worked
2016-05-02 10:01:23 -07:00
Girish Ramakrishnan ffedbdfa13 various minor fixes to eventlog 2016-05-02 10:01:23 -07:00
Johannes Zellner 43e207a301 Merge branch 'v0.12.7-hotfix' 2016-05-02 17:34:56 +02:00
Johannes Zellner bea6019dc4 Changes for 0.12.7 2016-05-02 14:20:59 +02:00
Johannes Zellner 9042e9e1a9 Fix usage of the password change route in webadmin 2016-05-02 14:14:44 +02:00
Johannes Zellner b855dee4cb Fix crash by providing required eventsource 2016-05-02 13:43:12 +02:00
Girish Ramakrishnan b322f6805f move authType into source 2016-05-01 21:53:44 -07:00
Girish Ramakrishnan ccc119ddec add appLocation to user login 2016-05-01 21:47:35 -07:00
Girish Ramakrishnan 994cbaa22a add event log in model code 2016-05-01 21:38:20 -07:00
Girish Ramakrishnan d7a34bbf68 remove profile action 2016-05-01 20:14:21 -07:00
Girish Ramakrishnan 1f31fe6f8f make user.remove and user.update add eventlog 2016-05-01 20:11:11 -07:00
Girish Ramakrishnan 37bdd2672b make user.create take auditSource 2016-05-01 20:01:34 -07:00
Girish Ramakrishnan 63702f836a move developer eventlogs to model code 2016-05-01 13:46:33 -07:00
Girish Ramakrishnan 5898428a6c use req.connection.remoteAddress 2016-05-01 13:29:11 -07:00
Girish Ramakrishnan f4a6c64956 make cloudron.activate take an auditSource 2016-05-01 13:27:57 -07:00
Girish Ramakrishnan f9d4d3014d remove reboot (user cannot initiate anyway) 2016-05-01 13:23:31 -07:00
Girish Ramakrishnan 8254337552 make cloudron.updateToLatest take an auditSource 2016-05-01 13:17:35 -07:00
Girish Ramakrishnan d811115f21 update schema.sql 2016-05-01 13:17:27 -07:00
Girish Ramakrishnan fec388b648 move backup eventlog to model 2016-05-01 13:17:23 -07:00
Girish Ramakrishnan a969e323a6 what if cron was a username 2016-05-01 11:48:29 -07:00
Girish Ramakrishnan 09584ac29c fix failing test 2016-04-30 23:26:45 -07:00
Girish Ramakrishnan 7967610f3f add user login to event log 2016-04-30 23:18:14 -07:00
Girish Ramakrishnan 727332fe66 save the userId as well 2016-04-30 23:04:54 -07:00
Girish Ramakrishnan 09595d1c43 make tests pass 2016-04-30 22:35:46 -07:00
Girish Ramakrishnan c4ad6c803f add certificate renew event 2016-04-30 22:27:33 -07:00
Girish Ramakrishnan fd1a00d280 better text for action 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 5c2a650681 add activity view 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 43051cea3b add app update event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 3d50a251ee store email in USER_ADD event 2016-04-30 20:25:20 -07:00
Girish Ramakrishnan 219df8babd pick up correct ip 2016-04-30 19:08:24 -07:00
Girish Ramakrishnan d3d9706a70 make response plural 2016-04-30 18:57:43 -07:00
Girish Ramakrishnan 8a3ad6c964 store the activation username 2016-04-30 14:32:35 -07:00
Girish Ramakrishnan 90719cd4d9 do not store entire manifest 2016-04-30 14:28:59 -07:00
Girish Ramakrishnan 30445ddab9 more changes 2016-04-30 14:14:02 -07:00
Girish Ramakrishnan 157fbc89b8 add eventlog route 2016-04-30 14:08:44 -07:00
Girish Ramakrishnan 71219c6af7 add eventlog hooks 2016-04-30 14:05:19 -07:00
Girish Ramakrishnan 934abafbd4 make actions seem like commands 2016-04-30 13:34:06 -07:00
Girish Ramakrishnan bc6e896507 add eventlog test 2016-04-30 13:02:57 -07:00
Girish Ramakrishnan ca8731c282 add source to events table 2016-04-30 12:56:23 -07:00
Girish Ramakrishnan c511019d79 remove jslint hint 2016-04-30 11:53:46 -07:00
Girish Ramakrishnan 992c4ee847 add some event enums 2016-04-30 11:49:51 -07:00
Girish Ramakrishnan 8c427553ba add eventlogdb tests 2016-04-30 10:16:27 -07:00
Girish Ramakrishnan c1df22f079 use JSON type (nice for querying) 2016-04-30 10:15:33 -07:00
Girish Ramakrishnan 7673ecde2f Add eventlog model file 2016-04-29 23:58:29 -07:00
Girish Ramakrishnan a9d0cf66fd Add eventlogdb 2016-04-29 23:58:24 -07:00
Girish Ramakrishnan db89784af8 add eventlog table 2016-04-29 23:58:20 -07:00
Girish Ramakrishnan 4143f903ad 0.13.0 changes 2016-04-29 20:50:55 -07:00
Girish Ramakrishnan 12820db4a5 fix typo in mysql config 2016-04-29 20:20:52 -07:00
Girish Ramakrishnan 8837cc5a3c update nginx version 2016-04-29 19:38:06 -07:00
Girish Ramakrishnan f2545e3def bump systemd to 229 2016-04-29 19:18:31 -07:00
Girish Ramakrishnan 7a72bf3f78 select mysql 5.7 2016-04-29 19:12:20 -07:00
Girish Ramakrishnan 87351f04ef use 16.04
among other things this will bump the mysql version bringing us
json type
2016-04-29 19:11:00 -07:00
Girish Ramakrishnan 4a04e0b52f use recommendation from raymii.org 2016-04-28 09:59:03 -07:00
Girish Ramakrishnan 5945bce00e add misssing arg 2016-04-26 16:21:26 -07:00
Girish Ramakrishnan d2a3925e04 add altDomain to install route 2016-04-26 14:45:58 -07:00
Girish Ramakrishnan 9c9f82e2c5 fix usage of waitForDns 2016-04-26 11:09:14 -07:00
Girish Ramakrishnan 8fd3ff0ccc 0.12.6 changes 2016-04-26 09:53:33 -07:00
Girish Ramakrishnan dec2fdb6bb merge altDomain into location field 2016-04-25 21:22:09 -07:00
Girish Ramakrishnan c581d2a52c stash altDomain in oldConfig 2016-04-25 21:22:08 -07:00
Girish Ramakrishnan e6e748e30d fix cname answer 2016-04-25 16:16:14 -07:00
Girish Ramakrishnan 36fddacf5c add more debugs 2016-04-25 16:06:50 -07:00
Girish Ramakrishnan 183c1608a6 it is fine for one or more ns to be dead 2016-04-25 15:58:42 -07:00
Girish Ramakrishnan 51c8f65e8d wait for altDomain on install as well
restore calls install when there is no lastBackupId
2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0da6e9a5b9 fix casing 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 31c7a17684 Infinity does not work 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan a1e2cd438e return altDomain in response 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 6b0e00e28b use app.fqdn 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 19948851e0 return altDomain as the app.fqdn if present 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan bfe4f75881 display altDomain as the title of the app 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 7f13594f01 fix two typos 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 4fafac035e remove options in waitForDns 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan ca41e6acfd remove attempt module 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 9893dd6640 make waitfordns get the zone itself 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 8f7e4c2053 Make waitForDns wait for cname 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan d037b13401 wait for alt domain dns 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 83c955d25b add a label for alt domain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0bb6d969a4 inject altDomain in the APP_ORIGIN and APP_DOMAIN env vars 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 6062a5bdd2 move exports to top because sysinfo backend source the error class 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 70ab492efa remove jslint header 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan aab035f7b9 use the acme backend when using altDomain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0e825272ae ensureCertificate now takes app object 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 46fee9e431 use config.adminFqdn instead 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0789c96992 altDomain ui 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan a4adc581fa make nginx.configure/unconfigure use altDomain 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 500fb452e7 use altDomain when present to configure certs and nginx 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan e11b762ea1 use vhost instead 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan f5d1726352 remove jslint header 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 3d5aa9fd23 pass altDomain in configure route 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan ef12740060 add altDomain to appdb fields 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 415902d68e add altDomain to apps table 2016-04-25 10:52:12 -07:00
Girish Ramakrishnan 0ef0e010a3 use defines for role names 2016-04-25 10:30:56 -07:00
Girish Ramakrishnan 2d27da89d2 validate individual scopes 2016-04-25 10:26:47 -07:00
Girish Ramakrishnan 9d8def8349 update bytes ejs-cli dockerode morgan superagent ursa validator x509 2016-04-22 22:26:32 -07:00
Girish Ramakrishnan 2533111bfa more 0.12.5 changes 2016-04-20 19:41:45 -07:00
Girish Ramakrishnan 20d6da8230 add debugs 2016-04-20 19:40:58 -07:00
Girish Ramakrishnan f159cacfbb Use same timestamp for archive and config
This fixes a very curious case:
1. App has backup.
2. App dies.
3. Box backs up. This make it reuse the backup. But it generates wrong config file timestamp.
4. Box cannot update anymore. This is because the backup of app fails - it tries to reuse
   the backup and that fails with AccessDenied because the timestamp above is wrong!
2016-04-20 19:37:00 -07:00
Girish Ramakrishnan 5e9ea98b66 ignore apps in errored state 2016-04-20 19:05:49 -07:00
Girish Ramakrishnan d87b7dcb75 fix typo 2016-04-20 12:56:35 -07:00
Girish Ramakrishnan 6eea2fef9a retry fetching icon
e2e randomly fails with EAI_AGAIN
2016-04-20 00:40:22 -07:00
Girish Ramakrishnan 34fd5f14a5 0.12.5 changes 2016-04-19 21:52:24 -07:00
Girish Ramakrishnan a4e73e747c fix crash mail subject 2016-04-19 19:12:47 -07:00
Girish Ramakrishnan eadff099eb send logs when apptask fails 2016-04-19 18:40:46 -07:00
Girish Ramakrishnan 15653cb3f8 rename to logcollector 2016-04-19 18:13:05 -07:00
Girish Ramakrishnan 2f8dc35c5d rename to sendFailureLogs 2016-04-19 18:06:11 -07:00
Girish Ramakrishnan a97720d204 rename to failure_notification 2016-04-19 18:04:45 -07:00
Girish Ramakrishnan 73898505b0 remove jslint header 2016-04-19 16:59:12 -07:00
Girish Ramakrishnan 88b4b6a38b use the crashnotifier module 2016-04-19 16:47:21 -07:00
Girish Ramakrishnan 3da82e3a63 rename to crashnotifierservice 2016-04-19 16:45:05 -07:00
Girish Ramakrishnan dad1585704 send crash notification on apptask crash 2016-04-19 16:43:58 -07:00
Girish Ramakrishnan e81dbdb36c add crashnotifier module 2016-04-19 16:42:05 -07:00
Girish Ramakrishnan ee2478e500 collect last 300 lines 2016-04-19 16:39:28 -07:00
Girish Ramakrishnan 0f7a6964a4 0.12.4 changes 2016-04-19 16:24:37 -07:00
Girish Ramakrishnan 5fa974ffe6 wait for 30 seconds in taskmanager instead
problem: because the apps are not inserted into appdb, the cloudron starts out
with an empty view. apps appear suddenly after 30 seconds.

besides, it makes more sense because 30 secs is not really tied to first run
2016-04-19 13:56:41 -07:00
Girish Ramakrishnan e1b7198a29 reverse setTimeout args 2016-04-19 12:28:36 -07:00
Girish Ramakrishnan 37d6354627 wait for 30 seconds for the addons to start up
The platform sometimes takes time to start up (especially in 1GB droplet).
This means that apps like wordpress begin auto installing and they fail
since mysql has not started yet.
2016-04-19 12:15:25 -07:00
Girish Ramakrishnan 6ab3e04fc1 0.12.3 changes 2016-04-19 12:11:59 -07:00
Girish Ramakrishnan b1987868be Set sn attribute only if non-empty
sn and givenName have as their superior the name attribute, which is of DirectoryString syntax,
that is, the syntax is 1.3.6.1.4.1.1466.115.121.1.15. Attributes which are of syntax
DirectoryString are not allowed to be null, that is, a DirectoryString is required to have
at least one character.

http://stackoverflow.com/questions/15027094/how-to-filter-null-or-empty-attributes-from-an-active-directory-query

This fixes a crash in paperwork which relies on this.
2016-04-19 12:03:03 -07:00
Girish Ramakrishnan 72eb3007c4 tmp -> obj 2016-04-19 12:00:34 -07:00
Girish Ramakrishnan 6c1da45ad1 skip the not automated part 2016-04-19 10:49:46 -07:00
Girish Ramakrishnan b9857cdb65 use async.retry 2016-04-18 22:06:49 -07:00
Girish Ramakrishnan d5c251115c typo 2016-04-18 18:57:29 -07:00
Girish Ramakrishnan 64c66e248b add mongodb test 2016-04-18 18:12:56 -07:00
Girish Ramakrishnan bb53c4f331 fix setup of mongodb 2016-04-18 18:05:23 -07:00
Girish Ramakrishnan 3215d4a3c9 move the exports to the top 2016-04-18 16:30:58 -07:00
Girish Ramakrishnan 68c4d77494 0.12.2 changes 2016-04-18 15:24:33 -07:00
Girish Ramakrishnan 44bf299e10 Merge remote-tracking branch 'origin/users' 2016-04-18 15:19:38 -07:00
Girish Ramakrishnan 6b1e14b464 add option to buffer stdout 2016-04-18 15:02:31 -07:00
Girish Ramakrishnan 8dcde84c3c remove memorystream 2016-04-18 14:56:47 -07:00
Girish Ramakrishnan a0deedb958 fixup backup and restore to use docker.execContainer 2016-04-18 14:56:01 -07:00
Girish Ramakrishnan a2096bec18 use options.stdout to pass back result 2016-04-18 12:22:42 -07:00
Girish Ramakrishnan 4f82bcec43 make execContainer take options arg 2016-04-18 11:42:34 -07:00
Girish Ramakrishnan 491356ce8d fix teardownMongoDb 2016-04-18 11:29:11 -07:00
Girish Ramakrishnan 6c99105a7e Make teardown commands use docker.execContainer 2016-04-18 11:25:16 -07:00
Girish Ramakrishnan 71f847776b fix up all the seutp code to use docker.execContainer 2016-04-18 11:15:21 -07:00
Girish Ramakrishnan 87c5371603 use docker exec instead of dockerode exec in mysql
this way we can check the exit code of the exec process.
preivously, we were only wait for the stream to end.
2016-04-18 11:06:09 -07:00
Girish Ramakrishnan 01d676628d rename docker variable 2016-04-18 10:37:33 -07:00
Girish Ramakrishnan 60badce935 add docker.execContainer 2016-04-18 10:32:22 -07:00
Johannes Zellner 182ae6bf1f Add some description to the upgrade dialog 2016-04-18 17:21:26 +02:00
Johannes Zellner c62ef9e156 Implement upgrade request dialog
This is currently merely a placeholder for some real upgrade ui
2016-04-18 17:21:26 +02:00
Johannes Zellner 96383a1fae Support upgrade_request feedback type 2016-04-18 17:11:36 +02:00
Johannes Zellner 5e9542ee76 Offer upgrade on install if resources are low 2016-04-18 16:16:44 +02:00
Johannes Zellner cc28d49df4 That api already returns the user array 2016-04-18 15:13:22 +02:00
Johannes Zellner 18f3733d6e Simplify the password change logic
We now can use verifyPassword and this makes
user.changePassword() route obsolete
2016-04-17 19:17:03 +02:00
Johannes Zellner 87dcf42c7e Remove redundant client api listUsers() 2016-04-17 18:42:56 +02:00
Johannes Zellner 32d8627045 Password change api is now in /profile 2016-04-17 18:41:13 +02:00
Johannes Zellner 6a607f9565 Adjust user route tests 2016-04-17 18:39:00 +02:00
Johannes Zellner c623770b44 Group /users routes better 2016-04-17 18:38:49 +02:00
Johannes Zellner 69f3620b22 remove unused user route functions 2016-04-17 18:27:11 +02:00
Johannes Zellner 21110bb2e0 Enable profile password change tests 2016-04-17 17:51:37 +02:00
Johannes Zellner fabe55622e Fix the first bunch of profile tests 2016-04-17 16:49:09 +02:00
Johannes Zellner 73e079cc6c Add initial profile route tests 2016-04-17 16:42:45 +02:00
Johannes Zellner a7d22a1972 Add specific user profile routes 2016-04-17 16:22:39 +02:00
Girish Ramakrishnan 5c1970b37f Fix crash where portBindings is set to undefined 2016-04-15 21:27:42 -07:00
Girish Ramakrishnan db065bd0fc 0.12.1 changes 2016-04-15 18:28:02 -07:00
Girish Ramakrishnan db6d8deec4 fix another typo 2016-04-15 18:25:46 -07:00
Girish Ramakrishnan 414b21f29a add sysadmin route test 2016-04-15 12:33:54 -07:00
Girish Ramakrishnan c4c7668b5a Use new env vars 2016-04-15 11:57:51 -07:00
Girish Ramakrishnan b9fa87cca2 cloudron.backup -> backups.backup 2016-04-15 11:57:51 -07:00
Girish Ramakrishnan 218c9099fd more CHANGES 2016-04-15 11:09:25 -07:00
Girish Ramakrishnan 916d97f7bd reserve the no-reply mailbox 2016-04-15 11:09:25 -07:00
Johannes Zellner 109f777c00 Fix focus in setup wizard 2016-04-15 14:49:10 +02:00
Johannes Zellner 4bf3a78227 Add display name input in setup wizard 2016-04-15 12:18:41 +02:00
Girish Ramakrishnan c03e69232e Remove admin name (already set in cloudron.conf now) 2016-04-14 20:39:05 -07:00
Girish Ramakrishnan 91a016ee91 Change the admin email to no-reply 2016-04-14 19:56:54 -07:00
Girish Ramakrishnan 8256f97e9d use latest mail image 2016-04-14 19:37:34 -07:00
Girish Ramakrishnan d095899aef add note that admin@fqdn is reserved as well 2016-04-14 13:34:41 -07:00
Johannes Zellner 6293c0aede Add test for reserved username 'admin' 2016-04-14 16:30:31 +02:00
Johannes Zellner 101ce62ef3 Move username and email lowercasing to where it belongs
Fixes #592
2016-04-14 16:25:48 +02:00
Girish Ramakrishnan 9f443e2d07 should ideally use shutdown commands at some point (for mongodb) 2016-04-13 20:53:07 -07:00
Girish Ramakrishnan 0a30585a05 bump mongodb (handles mongod crash recovery) 2016-04-13 20:35:20 -07:00
Girish Ramakrishnan ed78bd05c8 reserve the "admin" username 2016-04-13 16:50:20 -07:00
Girish Ramakrishnan c24d7e7b3c do not crash on duplicate email 2016-04-13 14:47:35 -07:00
Girish Ramakrishnan 389d2be82d CLI is not a mode 2016-04-13 11:11:04 -07:00
Girish Ramakrishnan 38b85e6006 set givenName and sn in ldap response 2016-04-13 10:52:25 -07:00
Johannes Zellner de2cde7333 Test oauth with mixed case username and email 2016-04-13 12:48:02 +02:00
Johannes Zellner 08410569c0 Actually fix the correct thing in the janitor tests 2016-04-13 12:43:18 +02:00
Johannes Zellner be3b08a7b4 Test case-insensitive developer login 2016-04-13 12:39:50 +02:00
Johannes Zellner 2724cfd0ad Test simpleauth with uppercase username input 2016-04-13 12:30:55 +02:00
Johannes Zellner d7c8cf5e0e Ensure ldap filter values are treated lowercase only 2016-04-13 12:28:44 +02:00
Johannes Zellner 11f89da3a0 Ensure username and email are treated lower case in the database layer 2016-04-13 12:15:49 +02:00
Johannes Zellner a803af2300 Revert "add get route for user"
This route is already there, the reason is, that the users api works off
the :userId but the profile api works off the req.user coming from the used
access token.

This reverts commit dbef4d71be5a68239133ab9b6e0fc1fd88ee27cd.
2016-04-13 11:36:49 +02:00
Johannes Zellner 6991402a8c Fix typo 2016-04-13 11:33:01 +02:00
Johannes Zellner 259798a8f2 Ensure auth code expiration is calculated at the right time 2016-04-13 11:32:30 +02:00
Johannes Zellner d83395ecfb Also test grant type token access tokens 2016-04-13 11:28:10 +02:00
Johannes Zellner 6d3dd452be Test that oauth tokens are actually usable after issuing 2016-04-13 11:03:35 +02:00
Johannes Zellner 40bee79e3d Fix oversight to store userId as user.username for auth codes 2016-04-13 10:45:11 +02:00
Girish Ramakrishnan 95de25560b add profile scope to developer tokens 2016-04-12 19:08:56 -07:00
Girish Ramakrishnan 79eee94a5e Fix setup link path 2016-04-12 18:40:00 -07:00
Girish Ramakrishnan 82651a33c7 typo 2016-04-12 18:16:52 -07:00
Girish Ramakrishnan 212a0ffcd9 add get route for user 2016-04-12 18:13:37 -07:00
Girish Ramakrishnan 115ed12c36 check that app patch releases does not send email 2016-04-12 13:49:49 -07:00
Girish Ramakrishnan 53268b67dc test: it does not send mail for box patch releases 2016-04-12 13:45:11 -07:00
Girish Ramakrishnan 40dd12ba68 verify emails are sent in updatechecker test 2016-04-12 13:24:23 -07:00
Girish Ramakrishnan 7a111e29ad test updatechecker emails 2016-04-12 13:15:40 -07:00
Girish Ramakrishnan 065c65317d create owner in app update checker test 2016-04-12 13:13:16 -07:00
Girish Ramakrishnan 91a5d711f4 test: create owner 2016-04-12 13:12:17 -07:00
Girish Ramakrishnan 9071ea6c5e test: fix prerelease version 2016-04-12 13:01:42 -07:00
Girish Ramakrishnan 34521735da skip email notification for patch releases 2016-04-12 12:30:13 -07:00
Girish Ramakrishnan b7f6dfb197 remove verbose from tar 2016-04-10 22:49:39 -07:00
Girish Ramakrishnan fa330b4652 remove redundant debug 2016-04-10 22:44:43 -07:00
Girish Ramakrishnan 3bdbcff811 add debug 2016-04-10 22:34:55 -07:00
Girish Ramakrishnan ea3bd6d71d remove trailing comma 2016-04-10 22:29:09 -07:00
Girish Ramakrishnan d5cc96b1ff clean up backups code 2016-04-10 22:24:01 -07:00
Girish Ramakrishnan 4ed368cdd8 remove getBackupUrl 2016-04-10 22:12:06 -07:00
Girish Ramakrishnan 5229222014 getBackupCredentials is never used 2016-04-10 22:09:29 -07:00
Girish Ramakrishnan 9b0aa331e1 remove unused function 2016-04-10 22:08:11 -07:00
Girish Ramakrishnan 70cc073b1c only add to backupdb when the backup succeeded 2016-04-10 21:55:08 -07:00
Girish Ramakrishnan 29502fd8af remove unused exports 2016-04-10 21:52:01 -07:00
Girish Ramakrishnan 8d75fcfe67 typo 2016-04-10 21:46:01 -07:00
Girish Ramakrishnan b2668579d6 pass appid to backup script 2016-04-10 21:41:53 -07:00
Girish Ramakrishnan ba663faa64 fix debug 2016-04-10 21:39:45 -07:00
Girish Ramakrishnan 8db76f6b70 backup swap is not required anymore 2016-04-10 20:55:59 -07:00
Girish Ramakrishnan 322e9faee7 rework backup code
move all the backup code into backups.js
2016-04-10 20:41:08 -07:00
Girish Ramakrishnan af9d489395 backup apps use aws-cli 2016-04-10 18:47:25 -07:00
Girish Ramakrishnan 4565291c1c use aws-cli to upload box backups 2016-04-10 18:22:05 -07:00
Girish Ramakrishnan be127ec313 fix failing test 2016-04-10 17:15:23 -07:00
Girish Ramakrishnan 8b3a44b33c Add getBackupCredentials to backups API 2016-04-10 11:01:59 -07:00
Girish Ramakrishnan 08b5d7003d expose getBackupCredentials from storage api 2016-04-10 10:55:59 -07:00
Girish Ramakrishnan 60cc4c988f bump mysql addon 2016-04-09 02:34:54 -07:00
Girish Ramakrishnan 68219748ec oops, bump postgresql 2016-04-09 01:07:46 -07:00
Girish Ramakrishnan cfb56d7eee install aws-cli tool (for backups) 2016-04-08 23:58:07 -07:00
Girish Ramakrishnan 4690616230 Add 0.12.0 changes proactively 2016-04-08 23:57:05 -07:00
Girish Ramakrishnan 96d625b866 bump the postgresql addon (required for gitlab) 2016-04-08 23:46:39 -07:00
Johannes Zellner 2e281f8554 Only directly callback if the config is not empty
apiServerOrigin is always set if the config was set
2016-04-08 17:29:14 +02:00
Johannes Zellner 5da5d86bc8 Pass billing through from the appstore to the cloudron config 2016-04-08 17:27:22 +02:00
Johannes Zellner 103c0bd688 Add initial upgrade button version
This is currently always hidden
2016-04-08 13:57:24 +02:00
Girish Ramakrishnan 275d8c2121 fix user create response 2016-04-06 10:20:32 -07:00
Girish Ramakrishnan 4c964bcaf8 set userid correctly in tokendb 2016-04-06 10:11:48 -07:00
Girish Ramakrishnan e6c2c77f03 set username for predictability 2016-04-06 09:18:00 -07:00
Girish Ramakrishnan 819095b465 order for predictable tests 2016-04-06 09:08:59 -07:00
Girish Ramakrishnan 1453fd3c54 order by username to make tests deterministic 2016-04-06 08:46:42 -07:00
Johannes Zellner 867278a0b6 Use verifyWithUsername() instead of verify() in simpleauth 2016-04-06 08:57:55 +02:00
Girish Ramakrishnan 382fca3cf2 minor rewording in password reset 2016-04-05 23:08:57 -07:00
Girish Ramakrishnan f210501e12 create -> set 2016-04-05 23:06:17 -07:00
Girish Ramakrishnan 499921e3af more changes 2016-04-05 18:45:34 -07:00
Girish Ramakrishnan db19df9395 Bump infra version to force app reconfigure
Required for collectd profiles to be regenerated
2016-04-05 17:36:30 -07:00
Girish Ramakrishnan 6e2067bfe7 fix memory.stat path 2016-04-05 17:24:54 -07:00
Girish Ramakrishnan 8eb1b374ef cgroups are not in system.slice anymore 2016-04-05 17:12:50 -07:00
Girish Ramakrishnan 1734555974 handle case where box is not activated 2016-04-05 12:23:27 -07:00
Girish Ramakrishnan 7136de4d08 add debugs 2016-04-05 12:07:37 -07:00
Girish Ramakrishnan 21e8bc1ce5 javascript much 2016-04-05 12:06:15 -07:00
Girish Ramakrishnan 13020be6e6 default app bundle to null, if absent 2016-04-05 12:00:33 -07:00
Girish Ramakrishnan 3b922ff8b2 Some 0.11.2 changes 2016-04-05 11:07:32 -07:00
Girish Ramakrishnan 69402d0079 check activation state for existing cloudrons without a first run file 2016-04-05 10:56:27 -07:00
Johannes Zellner 99850f1161 Support ldap DNs with userId, username and email 2016-04-05 16:32:12 +02:00
Johannes Zellner b205212bf2 Explicitly verifyWithUsername() and offer a verify() userId based 2016-04-05 16:27:04 +02:00
Johannes Zellner baf586b028 Add missing 'else' 2016-04-05 16:25:05 +02:00
Johannes Zellner 94faa3575c Ensure we lowercase all emails
This ensures the uniqueness of that field
2016-04-05 11:15:50 +02:00
Johannes Zellner 544c1474d1 Allow multiple empty usernames in the db 2016-04-05 10:54:09 +02:00
Johannes Zellner bb25279878 Fixup some bugs in the user handling ui 2016-04-05 10:11:04 +02:00
Johannes Zellner 4939f526d5 Fixup the user.js tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 68af03f401 Fixup ldap tests 2016-04-05 09:28:41 +02:00
Johannes Zellner f744fee708 password change route also now takes the userId 2016-04-05 09:28:41 +02:00
Johannes Zellner c7ceb29845 Remove unused setAdmin() from webclient 2016-04-05 09:28:41 +02:00
Johannes Zellner 56d9d5913d Fixup the user route tests 2016-04-05 09:28:41 +02:00
Johannes Zellner f7887228d3 Fix oauth session view tests and simpleauth tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 73ed0384ea Fixup group rest api tests 2016-04-05 09:28:41 +02:00
Johannes Zellner 3051d4c22a This is actually a callback, doh 2016-04-05 09:28:41 +02:00
Johannes Zellner b32a0bcfad Do not allow empty username on createOwner() 2016-04-05 09:28:41 +02:00
Johannes Zellner 61c79aab23 Add asserts for user.createOwner() 2016-04-05 09:28:41 +02:00
Johannes Zellner 9740ffd504 Remove displayName setting in user edit view 2016-04-05 09:28:41 +02:00
Johannes Zellner 435ec2365b fix sendError() args 2016-04-05 09:28:41 +02:00
Johannes Zellner ff3562b0e8 Show error page for invalid reset tokens 2016-04-05 09:28:41 +02:00
Johannes Zellner 3be5511e33 Ensure we pass the resetToken on error 2016-04-05 09:28:41 +02:00
Johannes Zellner c8604e95ab Prevent password reset for not activated user 2016-04-05 09:28:41 +02:00
Johannes Zellner bbaf4c77fd This is ejs not angular 2016-04-05 09:28:41 +02:00
Johannes Zellner 1c9fc3f3dc Make account setup view prettier 2016-04-05 09:28:41 +02:00
Johannes Zellner 577959f281 Ensure browser autofill is disabled 2016-04-05 09:28:41 +02:00
Johannes Zellner 8af01f2955 Give basic form feedback for account creation 2016-04-05 09:28:41 +02:00
Johannes Zellner c73213b2f2 Handle username conflict in account setup 2016-04-05 09:28:41 +02:00
Johannes Zellner 36f3f4b8f4 Remove superflous ' 2016-04-05 09:28:41 +02:00
Johannes Zellner 31bd5cdee3 Fix typo, userdb.del() wants an id 2016-04-05 09:28:41 +02:00
Johannes Zellner fd0326efb1 Allow to use username or password for user deletion form 2016-04-05 09:28:41 +02:00
Johannes Zellner 65c6806109 Send full user information on deletion, not just the uuid 2016-04-05 09:28:41 +02:00
Johannes Zellner 1b7406784e Fix the user invitation to use userId 2016-04-05 09:28:41 +02:00
Johannes Zellner 8cbf83058f Adjust the welcome mail 2016-04-05 09:27:32 +02:00
Johannes Zellner e058e22cae Fixup the client tests with userid change 2016-04-05 09:27:32 +02:00
Johannes Zellner c84674529b Calm down the app polling a bit 2016-04-05 09:27:32 +02:00
Johannes Zellner a0098a8883 Adjust email templates, as we do not have a username set now 2016-04-05 09:27:32 +02:00
Johannes Zellner f6547c9b71 Ensure we render a useful string for access restriction users 2016-04-05 09:27:32 +02:00
Johannes Zellner 6dc17183ee Do not use google font cdn 2016-04-05 09:27:32 +02:00
Johannes Zellner bba3dd5ec0 Fetch users and groups in apps view 2016-04-05 09:27:32 +02:00
Johannes Zellner 9eec6c2e9d Add an extra postprocess in client.js 2016-04-05 09:27:32 +02:00
Johannes Zellner c235b82660 Fallback to email if username is not set for single user apps 2016-04-05 09:27:32 +02:00
Johannes Zellner 67ac0fcd5a Add missing ajax-loader.gif for slick carousel 2016-04-05 09:27:32 +02:00
Johannes Zellner 87ca147e65 Show user and email in user selection for app permission 2016-04-05 09:27:32 +02:00
Johannes Zellner 0cf2bfb792 Use user.id for session serialization 2016-04-05 09:27:32 +02:00
Johannes Zellner a112e614e6 Actually query by username instead of just delegate to get() in getByUsername() 2016-04-05 09:27:32 +02:00
Johannes Zellner 0b1dcd2940 Use userdb.getByUsername() instead of get() 2016-04-05 09:27:32 +02:00
Johannes Zellner 951934f275 Remove unused require 2016-04-05 09:27:32 +02:00
Johannes Zellner 78518ff5f6 Hide username and displayName when adding an account 2016-04-05 09:27:32 +02:00
Johannes Zellner b8d0c01187 fix typo 2016-04-05 09:27:32 +02:00
Johannes Zellner 572e5c4938 Adjust the user add form to not require a username 2016-04-05 09:27:32 +02:00
Johannes Zellner e4fabd20c1 Do not require a username to be present when creating a user 2016-04-05 09:27:32 +02:00
Johannes Zellner 726d154890 Make user id a uuid.v4() and allow empty usernames 2016-04-05 09:27:32 +02:00
Johannes Zellner 7a5ac1a2f5 Add POST account/setup to distinguish between setup and password reset 2016-04-05 09:27:32 +02:00
Johannes Zellner c90a8041e2 Move password/setup.html -> account/setup.html 2016-04-05 09:27:32 +02:00
Johannes Zellner 18b91b5fa0 Rename password setup to account setup 2016-04-05 09:27:32 +02:00
Johannes Zellner f058c266d2 Add username and display name form fields on account setup 2016-04-05 09:27:32 +02:00
Johannes Zellner e0114c87ac Also update the user record when username and email is sent 2016-04-05 09:27:32 +02:00
Johannes Zellner c98275000b Optionally support username and email in password setter route 2016-04-05 09:27:32 +02:00
Girish Ramakrishnan 553509c462 implement installation of app bundle 2016-04-04 23:03:13 -07:00
Girish Ramakrishnan 306bef96b4 remove dead DNS_IN_SYNC 2016-04-04 22:14:05 -07:00
Girish Ramakrishnan 497eaea65e bump expiry to 60 mins 2016-04-04 16:02:13 -07:00
Girish Ramakrishnan 8aacc503a6 Revert "getRestoreUrl now uses caas restore api"
This reverts commit f9fc9325a8995dc0a9cb1dfcf22fb27eca697a89.

For now, we can simply assume that caas is s3 based.
2016-04-04 15:57:32 -07:00
Girish Ramakrishnan ec160fe45f make getBackupUrl return id as well 2016-04-04 12:45:09 -07:00
Girish Ramakrishnan 82c74e6787 add backupdb tests 2016-04-04 12:41:17 -07:00
Girish Ramakrishnan bbff195863 rename filename to id 2016-04-04 12:20:56 -07:00
Girish Ramakrishnan e528dbcfc0 creationTime is redundant 2016-04-04 12:13:54 -07:00
Girish Ramakrishnan 0467e80c71 remove unused require 2016-04-04 12:13:25 -07:00
Girish Ramakrishnan c9ef0056e0 rename getSignedUploadUrl to getBackupUrl 2016-04-04 12:01:47 -07:00
Girish Ramakrishnan efb228cf5e getRestoreUrl now uses caas restore api 2016-04-04 11:57:29 -07:00
Girish Ramakrishnan af700827c5 info is not passed anymore 2016-04-04 11:44:24 -07:00
Girish Ramakrishnan 3135783fe3 rename getSignedDownloadUrl to getRestoreUrl 2016-04-04 11:43:56 -07:00
Girish Ramakrishnan 496f530b9f sessionToken is required in credentials (when signing) 2016-04-04 11:23:38 -07:00
Girish Ramakrishnan f44c2707f0 install swaks in base image 2016-04-04 09:50:19 -07:00
Johannes Zellner 9fbbddc3eb Show setupLink properly 2016-04-04 18:41:51 +02:00
Johannes Zellner 5afb16aa98 Show dialog with setupLink on invite 2016-04-04 18:41:51 +02:00
Johannes Zellner 8f2b0bae5e Receive the resetToken in the webadmin 2016-04-04 18:41:51 +02:00
Johannes Zellner fcfd1dceac Deliver the resetToken when an invite was sent 2016-04-04 18:41:51 +02:00
Girish Ramakrishnan d839f0b762 remove redundant session token 2016-04-03 23:23:23 -07:00
Girish Ramakrishnan 16a65fb185 drop configJson
The initial idea was to store exactly where the backups are stored.
But this only causes problems for migrations where the bucket might
change and clones where the prefix (box.id) changes.

Thus, it's best to leave the url creation to the caas side. (That
has to be done in another change)
2016-04-03 22:55:08 -07:00
Girish Ramakrishnan aaeb355183 add version 0.11.1 changes 2016-04-03 11:34:47 -07:00
Girish Ramakrishnan c236072c4c add comment 2016-04-02 18:04:58 -07:00
Girish Ramakrishnan 5d92cff638 backup config.json first because tarball takes lot of time and leads to token expiration 2016-04-02 18:01:49 -07:00
Girish Ramakrishnan 1b539b8d22 upload as binary 2016-04-02 17:58:10 -07:00
Girish Ramakrishnan a21a913f34 delete snapshot on failure path 2016-04-02 17:57:15 -07:00
Girish Ramakrishnan 357f6f0552 use same region as what we uploaded to 2016-04-02 13:32:14 -07:00
Girish Ramakrishnan b16aa4c007 check for region as well 2016-04-02 13:31:12 -07:00
Girish Ramakrishnan 1fed5ee353 0.11.0 changes 2016-04-01 23:38:35 -07:00
Girish Ramakrishnan 29077abf7c pass back the changeId 2016-04-01 23:21:10 -07:00
Girish Ramakrishnan f5c7116573 Add 0.10.4 changelog 2016-04-01 13:59:07 -07:00
Girish Ramakrishnan 42fc2d446c do not set the session_token header
this seems to be part of url now in signature v4
2016-04-01 13:26:25 -07:00
Girish Ramakrishnan 9ef04dc67f more 0.10.3 changes 2016-03-31 10:48:08 -07:00
Girish Ramakrishnan 3ea2070cdb pass filename (it is not part of the config!) 2016-03-31 09:53:56 -07:00
Girish Ramakrishnan fc11484b51 pick region from apiConfig if present 2016-03-31 09:48:38 -07:00
Girish Ramakrishnan b4ddfa94cc rename to apiConfig 2016-03-31 09:48:13 -07:00
Girish Ramakrishnan 9e7ae1a4f7 we really need a better linter 2016-03-31 09:38:40 -07:00
Girish Ramakrishnan d27159275b pick region as well 2016-03-31 09:34:57 -07:00
Girish Ramakrishnan 6c2ae756f1 fix usage of backupInfo 2016-03-31 09:23:41 -07:00
Girish Ramakrishnan 92e4433dff make backupdb.get return app backups 2016-03-31 09:12:12 -07:00
Girish Ramakrishnan c4cbd9f4e4 mailer: check for the correct SPF record 2016-03-31 08:44:31 -07:00
Johannes Zellner f413afb835 Support email login in simple auth 2016-03-31 16:59:44 +02:00
Johannes Zellner 915c37a72f Add tests for displayName change 2016-03-31 16:24:28 +02:00
Johannes Zellner 1ddb3a58da Also send displayName in simpleAuth 2016-03-31 16:24:28 +02:00
Girish Ramakrishnan a4aa5bbc59 fix linting 2016-03-31 00:51:38 -07:00
Girish Ramakrishnan 39cc5d07d1 use the backupdb config to determine bucket and prefix 2016-03-31 00:50:56 -07:00
Girish Ramakrishnan f3a05931df fix typo 2016-03-30 23:43:04 -07:00
Girish Ramakrishnan df39384056 do not save backup secrets in database 2016-03-30 23:39:48 -07:00
Girish Ramakrishnan 47c5cad239 fix typo 2016-03-30 17:06:50 -07:00
Girish Ramakrishnan ec380aa41e each change really needs to be in separate line 2016-03-30 15:34:09 -07:00
Girish Ramakrishnan 7d1a663a87 0.10.3 changes 2016-03-30 15:26:52 -07:00
Girish Ramakrishnan ba69316c14 add note that filename is reused as id 2016-03-30 15:17:04 -07:00
Girish Ramakrishnan c097651a88 store backup configuration as part of backups table 2016-03-30 15:04:39 -07:00
Girish Ramakrishnan 22b8154a39 0.11.0 changes 2016-03-30 11:39:50 -07:00
Girish Ramakrishnan 9e8179a235 up link is relative 2016-03-29 14:02:53 -07:00
Girish Ramakrishnan 3fbeb2a1c1 more 0.10.2 changes 2016-03-29 13:24:26 -07:00
Girish Ramakrishnan 2c4cf0a505 Download intermediate cert following the 'up' Link 2016-03-29 12:51:05 -07:00
Girish Ramakrishnan adab544e99 Version 0.10.2 changes 2016-03-28 10:55:20 -07:00
Girish Ramakrishnan ae8a371597 add adminFqdn in the spf record
For custom domains, we do not set the A record for the naked domain
(because the user might be using it for his own). This means that
a:domain.com will not work.

The solution is to simply use the admin domain.
2016-03-27 23:05:29 -07:00
Girish Ramakrishnan ead076bd9f add MAIL_SMTP_PASSWORD 2016-03-25 23:14:09 -07:00
Girish Ramakrishnan f8c683f451 Disallow updating an app with mismatching manifest id
Story so far:
1. App installed from store. appStoreId is set to manifest.id.
2. User installed a custom built app with a custom manifest.id using cloudron install --app <id>. The appStoreId is still set.
3. When we make a new release, it overrides the users install.

The fix (for now) is:
1. Do not allow mismatching ids to start with.
2. When forced, it is allowed but appStoreId is cleared so as to not get any auto updates.

This leaves the user vulnerable to 'cloudron uninstall' simply autoselecting this new app.
For this, they have to simply disable CLI mode for now.

There is also a corner case where:
1. Dev installs from app store
2. Dev compiles from source and updates on top of app store install with --app <id>
3. Dev find out that his installation has auto-updated the next day.
2016-03-25 11:46:25 -07:00
Johannes Zellner b56bc08e9a Allow to use email and username for ldap bind 2016-03-24 21:03:04 +01:00
Girish Ramakrishnan daadbfa23f fix wording 2016-03-23 12:00:30 -07:00
Girish Ramakrishnan a215443c56 do not renew apps without any cert
autoRenew was mistakenly reconfiguring app without a cert (this
is the common case for apps in non-custom domain)
2016-03-23 08:49:08 -07:00
girish@cloudron.io 4e22c6d5ac minor nakedomain fixes 2016-03-21 15:07:10 -07:00
girish@cloudron.io d43810fea9 add comment on why we add naked domain for custom domains 2016-03-21 13:50:26 -07:00
girish@cloudron.io f5ab63e8ec naked domain page styling 2016-03-21 13:49:11 -07:00
girish@cloudron.io b1f172ed17 trim the output string 2016-03-21 08:25:10 -07:00
Girish Ramakrishnan 413f9231b3 fix formatting 2016-03-20 12:12:22 -07:00
Girish Ramakrishnan 11513f9428 send a message for cert renewal status 2016-03-19 20:40:03 -07:00
Girish Ramakrishnan 5042741435 renew cert every 12 hours 2016-03-19 20:30:01 -07:00
Girish Ramakrishnan 75ed9c4a63 Check for key file instead of csr file
1) csr file in older backups got corrupt
2) new key results in a new cert request in LE (for rate limits)
2016-03-19 18:49:55 -07:00
Girish Ramakrishnan 8c36f3aab4 add debug for fallback case 2016-03-19 18:37:05 -07:00
Girish Ramakrishnan 7aa5e8720a 0.10.1 changes 2016-03-19 14:17:28 -07:00
Girish Ramakrishnan 14ef71002f write the DER cert properly into the csr file 2016-03-19 14:07:58 -07:00
Girish Ramakrishnan ea87841e77 merge fallback cert job into renewal
this is becase we need to reconfigure for the case where we got a
renewed cert (but the app was switched to fallback cert at some point)
2016-03-19 13:54:52 -07:00
Girish Ramakrishnan 091e424c0e Fix description 2016-03-19 13:37:58 -07:00
Girish Ramakrishnan 20629ea078 fix linter errors 2016-03-19 13:22:38 -07:00
Girish Ramakrishnan b1b6a9ae65 reconfigure admin using configureAdmin 2016-03-19 12:54:11 -07:00
Girish Ramakrishnan 7ddbf7b652 refactor expiry check 2016-03-19 12:50:31 -07:00
Girish Ramakrishnan 3d088aa9c4 fix debug message 2016-03-19 12:31:48 -07:00
Girish Ramakrishnan f329e0da92 fix typo 2016-03-19 12:14:23 -07:00
Girish Ramakrishnan a18737882b run more aggressively in test mode 2016-03-19 12:12:39 -07:00
Girish Ramakrishnan a58a458950 do not abbrev 2016-03-19 12:11:28 -07:00
Girish Ramakrishnan 44c5f84c56 Fix usage of isExpiringSync 2016-03-19 12:06:13 -07:00
Girish Ramakrishnan d6b92ee301 remove Job suffix 2016-03-19 10:25:19 -07:00
Girish Ramakrishnan c769a12c45 set the box version for test for pass 2016-03-19 10:23:12 -07:00
Girish Ramakrishnan 017c32c3dd fix certificate renewal
Do the whole acme flow for certificate renewal. the idea here is
simply reuse the key and the csr. In this case, it does not count
as a new certificate issuance.

https://github.com/diafygi/letsencrypt-nosudo/issues/55
2016-03-19 02:44:05 -07:00
Girish Ramakrishnan 5d54c9e668 check my domain for expiry and falling back 2016-03-18 23:43:56 -07:00
Girish Ramakrishnan adaaca5ceb switch expired certs of domains to use fallback cert
1) nginx won't reload when using expired certs
2) this is the only way the user can use the app now
2016-03-18 23:26:57 -07:00
Girish Ramakrishnan 4a73e1490e Refactor code to take hours 2016-03-18 23:00:02 -07:00
Girish Ramakrishnan f31a7a5061 use fallback certs if renewal fails 2016-03-17 12:20:02 -07:00
Girish Ramakrishnan 3499a4cc6c move requiresOAuthProxy to nginx
we have 3 levels
    * routes, cron, apptask
    * everything else where everyone calls everyone :-)
    * the db layer
2016-03-17 11:38:29 -07:00
girish@cloudron.io 42796b12dc update safetydance to 0.1.1 2016-03-14 22:50:48 -07:00
girish@cloudron.io 20ac040dde cert: check expiry correctly 2016-03-14 22:50:06 -07:00
girish@cloudron.io 7f2b3eb835 acme: disable renewal via url fetch for now
this does not seem to work.

From https://github.com/ericchiang/letsencrypt/commit/cf85854177b22540ca1aeba770c2b86534c6c5ef:

// RenewCertificate attempts to renew an existing certificate.
// Let's Encrypt may return the same certificate. You should load your
// current x509.Certificate and use the Equal method to compare to the "new"
// certificate. If it's identical, you'll need to run NewCertificate and/or
// start a new certificate flow.
2016-03-14 22:22:57 -07:00
girish@cloudron.io 2b562f76ea le: handle renewal upto 30 days in advance 2016-03-14 22:18:43 -07:00
Girish Ramakrishnan b942033512 acme: debug output the domain 2016-03-14 16:21:03 -07:00
Girish Ramakrishnan fa4a8c2036 add debug for successful renewal 2016-03-14 15:55:51 -07:00
Johannes Zellner 27febbf1e9 The blue cloud is gone 2016-03-11 16:48:56 -08:00
girish@cloudron.io 8da2eb36cc fix email wording 2016-03-09 18:37:02 -08:00
girish@cloudron.io cbb34005c6 restoreKey -> filename 2016-03-09 14:23:42 -08:00
girish@cloudron.io efc1627648 more changes 2016-03-09 09:34:57 -08:00
girish@cloudron.io f513dcdf3b Add 0.10.0 changelog 2016-03-09 09:29:17 -08:00
girish@cloudron.io 61a52d8888 dist-upgrade to update more aggressively 2016-03-09 09:29:07 -08:00
Johannes Zellner 4cfc187063 Add sender name to admin email 2016-03-09 07:41:50 +01:00
Johannes Zellner 065af03e5f Stop docker proxy in ldap tests 2016-03-09 07:34:44 +01:00
Johannes Zellner c4eeebdfbe Enable admin change test 2016-03-09 06:18:39 +01:00
Johannes Zellner b1004de358 Notify admins about newly added admin 2016-03-09 06:16:21 +01:00
Girish Ramakrishnan fbca0fef38 fix missing assert 2016-03-08 18:51:40 -08:00
Girish Ramakrishnan d658530e66 fix failing tests 2016-03-08 18:44:51 -08:00
Girish Ramakrishnan 21d4cc9cb2 getAllPaged -> getPaged 2016-03-08 18:10:39 -08:00
Girish Ramakrishnan e2b7ec3ffd store filename with tar.gz extension 2016-03-08 16:47:53 -08:00
Girish Ramakrishnan 8014e2eaf8 add route to download backup 2016-03-08 16:28:42 -08:00
girish@cloudron.io a10ed73af2 get zoneName using tldjs 2016-03-08 09:52:13 -08:00
girish@cloudron.io 8b2903015d list app backups from db 2016-03-08 08:57:28 -08:00
girish@cloudron.io d157bf30f3 remove box backups from the database 2016-03-08 08:52:20 -08:00
girish@cloudron.io 7996b32022 add backups to the database
ideally, these should be done _after_ the backup is successful and not when
the backup url is generated.

we had a discussion on why need backupdb to start with. Some rationale includes:
1. we can use it as a FK constraint (like make sure you delete backups when you delete app)
2. have labels for backups
3. dependancy relation from box backup to apps
4. s3 reverse sort is little painful and requires us to get all items in bulk and sort in JS
   (also requires us to change our backup filename format)
5. any metadata storage requires database

The cons include:
1. s3 and this db go out of sync
2. db entry is useless if s3 file is missing
2016-03-08 08:42:00 -08:00
girish@cloudron.io 4b77703902 export getByAppIdPaged 2016-03-07 17:52:13 -08:00
Girish Ramakrishnan 4dd82d10ad backup: ensure same timestamp for app data and config 2016-03-07 12:13:54 -08:00
Girish Ramakrishnan 83d05c99d3 mount manually instead of fstab because of race
I cannot figure how to make the box-setup.service run before the mounting
of a specific mount point. adding a dep on mount.target locks up the system.
2016-03-07 10:48:09 -08:00
Girish Ramakrishnan b0acdfb908 use truncate instead of fallocate 2016-03-07 10:44:35 -08:00
Girish Ramakrishnan b062dab65c mysql also uses the data partition 2016-03-07 10:38:59 -08:00
Girish Ramakrishnan eadcdeee1c not being mounted is the normal case 2016-03-07 10:37:26 -08:00
Girish Ramakrishnan 9de6f9c1c2 add backupdb
mostly same code as the appstore side
2016-03-07 09:30:44 -08:00
Girish Ramakrishnan 89f54245f7 Add backups table 2016-03-07 09:27:10 -08:00
Girish Ramakrishnan 5fbd1dae30 bump the mysql memory limit
we hit this memory limit often in phabricator backup. this is all
very crude but should suffice for now.
2016-03-05 18:35:28 -08:00
girish@cloudron.io 486ced0946 fix LDAP debug 2016-03-04 17:52:27 -08:00
girish@cloudron.io d1c1fb8786 fix ldap debug ("ldap" already appears as part of debug) 2016-03-04 17:51:18 -08:00
Johannes Zellner 57ff8b6770 fix feedback test 2016-03-04 22:27:18 +01:00
Johannes Zellner d12d8f5c0b Properly extract referrers, which contain queries on their own and are not properly encoded 2016-03-03 15:06:14 +01:00
Johannes Zellner 17deac756b Also log app manifest id for alive apps 2016-03-03 09:30:46 +01:00
Johannes Zellner f7bb3bac98 Log app manifest id in healthmonitor 2016-03-03 09:30:46 +01:00
girish@cloudron.io 744c721000 use docker 1.10.2 (untested) 2016-03-01 10:13:44 -08:00
girish@cloudron.io 0500bae221 install aufs tools
https://github.com/docker/docker/issues/915
2016-03-01 10:13:04 -08:00
girish@cloudron.io a7b5b49d96 fix language 2016-02-26 10:14:37 -08:00
Johannes Zellner 93ef1919c2 Hide superuser checkbox for the user himself 2016-02-26 18:08:56 +01:00
girish@cloudron.io 254d6ac92e handle singular case as well 2016-02-26 08:43:59 -08:00
girish@cloudron.io 3a12265f42 Do not preallocate data volume
This is not tested but will get tested in the next upgrade
2016-02-26 08:27:13 -08:00
Johannes Zellner 71eeb47f0f Hide groups in user listing if the screen is too tiny 2016-02-26 13:10:29 +01:00
Johannes Zellner 5ea5023d97 Better encapsulate the form related functions in install view 2016-02-26 12:50:05 +01:00
Johannes Zellner 1148e21cd4 Add more changes 2016-02-26 12:29:52 +01:00
Johannes Zellner e9a2b2a7cf Disable group access control and show info if there are no groups 2016-02-26 11:59:18 +01:00
Johannes Zellner 7a34f40611 Allow to specify accessRestrictions on app install 2016-02-26 11:47:20 +01:00
Johannes Zellner c630de1003 Fetch groups in appstore.js 2016-02-26 11:24:01 +01:00
Johannes Zellner 74da8f5af8 Add 0.9.3 changes 2016-02-26 11:19:45 +01:00
girish@cloudron.io b758be5ae2 0.9.2 changes 2016-02-26 11:17:36 +01:00
Johannes Zellner c585be4eec Adjust group length error message 2016-02-26 11:07:31 +01:00
Johannes Zellner 3ebc569438 Avoid using superuser in the ui, but describe what it is 2016-02-26 11:03:39 +01:00
Johannes Zellner 5a2cf3cbfe Move superuser checkbox at the bottom of the form 2016-02-26 11:02:43 +01:00
Johannes Zellner 715c5f9f61 Do not set admin group multiple times 2016-02-26 10:56:33 +01:00
Johannes Zellner 6843fda601 Show group names and make sure we don't break layout right away 2016-02-26 10:53:41 +01:00
Johannes Zellner a78f3b1db3 Give more space to some views 2016-02-26 10:52:47 +01:00
girish@cloudron.io 1419108a86 umount is for unmounting 2016-02-25 20:13:16 -08:00
girish@cloudron.io 7a8b457ce9 truncate to shrink the file if required 2016-02-25 19:26:46 -08:00
girish@cloudron.io 10967ff8ce allow 1.2 times RAM
This is basically to allow 2 phabricators and another small app with
no warning on a 4gb droplet :-)
2016-02-25 18:34:28 -08:00
girish@cloudron.io 1fdfd3681c Revert "Display group ids"
This reverts commit d80ce25363061f95cb14e223efd4ab9828739eea.

Didn't mean to commit this
2016-02-25 18:11:25 -08:00
girish@cloudron.io 187d4f9ca2 round memory to nearest GB
os.totalmem returns some close-to-GB number. Because of this two
apps with 2GB don't install on 4GB.
2016-02-25 17:19:39 -08:00
girish@cloudron.io 6b67e64bf1 Display group ids 2016-02-25 16:33:58 -08:00
girish@cloudron.io 7ae6061d72 Edit -> Save 2016-02-25 15:24:50 -08:00
Johannes Zellner e96b9c3e3f Give the user the perception he gets what he pays for 2016-02-26 00:18:47 +01:00
Johannes Zellner c9ca05a703 Do not offer admin group for access restriction
The same can be achieved with a new group and it just
keeps the superuser/admin out of the way here. In any case
admins can always access all apps.
2016-02-26 00:13:09 +01:00
Johannes Zellner 23e5bed247 Fix the group numbering to ignore admin group 2016-02-26 00:02:38 +01:00
Johannes Zellner bae0d728b3 Only show group count to not break layout and allow quickedit 2016-02-26 00:01:25 +01:00
Johannes Zellner 5cd1c7d714 add hand selector 2016-02-26 00:01:01 +01:00
Johannes Zellner d430e902bf Separate superuser checkbox from the other groups in user edit 2016-02-25 23:20:55 +01:00
Johannes Zellner 4fb89de34f Remove 'this is you' 2016-02-25 22:29:06 +01:00
Johannes Zellner 7cd3bb31e1 Add new support type for failing erroring apps 2016-02-25 21:38:37 +01:00
Girish Ramakrishnan 2857158543 do not set memoryLimit 2016-02-25 11:05:19 -08:00
Johannes Zellner 82a347ea4b Add tooltips for superusers 2016-02-25 16:07:31 +01:00
Johannes Zellner b5c7f978a2 Do not show admin group in group listing 2016-02-25 15:54:30 +01:00
Johannes Zellner 625da29fce Show admins with an icon instead of a group tag 2016-02-25 15:53:36 +01:00
Johannes Zellner b82b183df6 Show alternative text for non admins, when no apps are available for this user 2016-02-25 15:38:46 +01:00
Johannes Zellner ce36fadf2b Fix bug for non admins to view the appstore 2016-02-25 15:34:44 +01:00
Johannes Zellner 2429599733 Change text from installed to your applications 2016-02-25 15:34:33 +01:00
Johannes Zellner 261a0a1728 Add account ui to change displayName 2016-02-25 15:09:52 +01:00
Johannes Zellner d8def61f67 Encapsulate the business logic in the account controller 2016-02-25 14:58:26 +01:00
Johannes Zellner 2732af24c1 Some code cleanups and bugfixes for the accounts view 2016-02-25 14:46:53 +01:00
Johannes Zellner 3d48da0e8d Remove unused function in client to change email 2016-02-25 14:34:35 +01:00
Johannes Zellner d3b8bd1314 Remove password field for user email change 2016-02-25 14:34:16 +01:00
Johannes Zellner f600ebcf19 Remove password entry from user edit form 2016-02-25 14:15:48 +01:00
Johannes Zellner 160467e199 Do not require password for user profile changes 2016-02-25 14:03:42 +01:00
Johannes Zellner 384c410e7c Do not require a password for user profile changes 2016-02-25 13:54:44 +01:00
Johannes Zellner 84c4187fa9 Test normal users accessing the user api 2016-02-25 13:53:18 +01:00
Johannes Zellner 4f7fd9177c Allow user details only for the same user or admins 2016-02-25 13:44:53 +01:00
Johannes Zellner b5b0ab7475 Require admin rights for user listing 2016-02-25 13:43:15 +01:00
Johannes Zellner a0d7406b3c Do not allow normal users to get group listings or details 2016-02-25 13:34:01 +01:00
Johannes Zellner 7165be0513 Warn the user about installing too many apps, but give an override button 2016-02-25 12:41:15 +01:00
Johannes Zellner 9c995277f7 Fixup the apps unit tests 2016-02-25 12:20:18 +01:00
Johannes Zellner aa693e529b Only list apps where a user has access to 2016-02-25 12:20:11 +01:00
Johannes Zellner 63013c7297 Just check for .admin flag in the user object 2016-02-25 11:42:25 +01:00
Johannes Zellner c8db6419d8 Admins are not special cased in apps.js app listing
This is done in the route
2016-02-25 11:41:14 +01:00
Johannes Zellner 93c1ddd982 Always amend the admin flag for further use 2016-02-25 11:40:48 +01:00
Johannes Zellner df102ec374 Add basic getAllByUser() app tests 2016-02-25 11:28:45 +01:00
Johannes Zellner 9688e4c124 Add apps.getAllByUser() 2016-02-25 11:28:29 +01:00
Johannes Zellner 00d277b1c3 Make the error dialog generic not only for app install errors 2016-02-24 18:36:40 +01:00
Johannes Zellner 0fb44bfbc1 Forward the error.message instead of making a new Error object
That leads to only Internal Error
2016-02-24 18:12:31 +01:00
Johannes Zellner c167bd8996 Set error in installationProgress also on uninstallation errors 2016-02-24 17:53:21 +01:00
Johannes Zellner a3737c3797 Report access denied errors in route53 backend 2016-02-23 17:29:28 +01:00
Johannes Zellner 8fcb0b46a5 add 0.9.1 changes 2016-02-21 14:51:51 +01:00
Johannes Zellner f5189e0a56 Force the user to set at least one access restriction group 2016-02-19 18:02:51 +01:00
Johannes Zellner 86f14b0149 Thanks JS for being so value focused... 2016-02-19 17:23:09 +01:00
Johannes Zellner 30913006e3 Remove all occurances of oauthProxy in the webadmin 2016-02-19 16:50:25 +01:00
Johannes Zellner 81bd4f2ea5 Remove console.log() 2016-02-19 16:29:40 +01:00
Johannes Zellner 351ddcb218 Fixup the unit tests 2016-02-19 16:29:29 +01:00
Johannes Zellner dd18f9741a Dynamically detect oauth proxy needs in apptask 2016-02-19 16:18:47 +01:00
Johannes Zellner cdce6e605d Adjust to new apps api 2016-02-19 16:14:02 +01:00
Johannes Zellner d4480ec407 Remove oauthProxy usage in the database wrapper 2016-02-19 16:12:58 +01:00
Johannes Zellner 85c92ab0b4 Remove oauthProxy from the apps.js api 2016-02-19 16:07:49 +01:00
Johannes Zellner 230c24d6c6 Adjust the route unit tests, to remove oauthProxy 2016-02-19 16:01:08 +01:00
Johannes Zellner 07c935dfec Remove oauthProxy from client side api wrapper 2016-02-19 16:00:48 +01:00
Johannes Zellner eab3bda8e1 Remove oauthProxy from the apps rest routes 2016-02-19 15:54:01 +01:00
Johannes Zellner f731c1ed0b Dynamically detect if an oauth proxy should be used for an app 2016-02-19 15:44:15 +01:00
Johannes Zellner edec3601f4 Do not rely on oauthProxy property of app object in nginx configuration code 2016-02-19 15:43:39 +01:00
Johannes Zellner 9e87fd0440 Add apps.requiresOAuthProxy() 2016-02-19 15:03:36 +01:00
Johannes Zellner 8cb304e1c9 Ensure app ordering by location 2016-02-19 13:36:39 +01:00
Johannes Zellner a24335d68b Remove oauth proxy setting ui 2016-02-19 12:04:05 +01:00
Johannes Zellner 78d1ed7aa5 Special case the admin button in apps access control UI 2016-02-18 18:26:24 +01:00
Johannes Zellner deb30e440a Disable ejs debug flag 2016-02-18 18:00:23 +01:00
Johannes Zellner 86ef9074b1 Add access restriction tests for ldap auth 2016-02-18 17:40:53 +01:00
Johannes Zellner 1a13128ae1 Ensure accessRestriction is either an object or an empty string 2016-02-18 17:28:00 +01:00
Johannes Zellner b41642552d The ldap property is part of req.connection 2016-02-18 16:40:30 +01:00
Johannes Zellner f5570c2e63 Enable the apps group access control ui 2016-02-18 16:14:45 +01:00
Johannes Zellner b0d11ddcab Adhere to access control on ldap user bind 2016-02-18 16:04:53 +01:00
Johannes Zellner 804464c304 Add apps.getByIpAddress() 2016-02-18 15:43:46 +01:00
Johannes Zellner ecf7f442ba Add docker.getContainerIdByIp() 2016-02-18 15:39:27 +01:00
Johannes Zellner 9ddd3aeb07 Show app id and fix naked domain in debugApp() 2016-02-18 12:51:25 +01:00
Johannes Zellner 864e3ff217 Give form feedback if group name is invalid
Fixes #583
2016-02-15 13:09:58 +01:00
Johannes Zellner 9bf1fe3b7d Show naked_domain for healthtask summary 2016-02-14 17:42:52 +01:00
Johannes Zellner b32a48c212 Add changelog for 0.9.0 2016-02-14 13:19:12 +01:00
Johannes Zellner 22a3dd7653 Disable advanced accessControl ui
This is working, from a configuration standpoint.
However not all auth methods support this yet, so
we hide it until that is done, otherwise it is just
confusing
2016-02-14 13:15:09 +01:00
Johannes Zellner 132b463e0a Hide memoryLimit ui 2016-02-14 13:14:03 +01:00
Johannes Zellner 7aefe5226a Properly layout the group labels 2016-02-14 13:12:04 +01:00
Johannes Zellner 656c1bfd3a Make Admin group members visible 2016-02-14 13:11:49 +01:00
Johannes Zellner e237b609f5 Make admin group buttons red 2016-02-14 13:08:50 +01:00
Johannes Zellner 057b9e954e Support btn-admin 2016-02-14 13:08:40 +01:00
Johannes Zellner f79c00d9be Special case admin group button in user profile 2016-02-14 12:51:58 +01:00
Johannes Zellner 5f96d862ab Move default memory limit to constants.js 2016-02-14 12:13:49 +01:00
Johannes Zellner 79199bf023 Ensure we stash and restore the memoryLimit 2016-02-14 12:10:22 +01:00
girish@cloudron.io beec4dddca my -> our 2016-02-13 11:22:47 -08:00
Johannes Zellner 7c243cb219 Warn the user on group deletion if it still has members 2016-02-13 12:42:41 +01:00
Johannes Zellner 754e33af2a do not allow removing the admin group 2016-02-13 12:42:41 +01:00
Johannes Zellner 63cab7d751 Allow non-empty groups to be deleted 2016-02-13 12:42:41 +01:00
Johannes Zellner 503714a10b Special case admin group in group listing 2016-02-13 12:42:41 +01:00
girish@cloudron.io ada5be6ae0 Add 0.9.0 changes 2016-02-13 03:27:21 -08:00
girish@cloudron.io 2112494b43 bump mysql image version 2016-02-13 03:26:29 -08:00
girish@cloudron.io c0b45ad71e update manifestformat to 2.3.0 2016-02-12 17:40:00 -08:00
girish@cloudron.io 5669d387af check app state before exec 2016-02-12 12:32:58 -08:00
Johannes Zellner 957f20a9a8 Always store the memory limit in the app db record and adjust on update if needed 2016-02-11 18:14:16 +01:00
Johannes Zellner 71bfc1cbda Ensure we never go below minimum memoryLimit 2016-02-11 18:13:42 +01:00
Johannes Zellner 489ea3a980 Add memoryLimit validation 2016-02-11 17:39:15 +01:00
Johannes Zellner 8c6f655628 Ensure we deal with byte values for memoryLimit 2016-02-11 17:29:00 +01:00
Johannes Zellner 75d22d7988 Introduce memoryLimit to apps routes 2016-02-11 17:00:21 +01:00
Johannes Zellner a7bf043a9e Improve memory limit ui 2016-02-11 16:31:11 +01:00
Johannes Zellner 402385faca Remove unused linter options 2016-02-11 14:35:51 +01:00
Johannes Zellner cdd82fa456 Add access restriction by group ui to app configure dialog 2016-02-11 14:14:51 +01:00
Johannes Zellner 2f7d99f3f6 Do not allow to remove the user from the admin group 2016-02-11 13:43:02 +01:00
Johannes Zellner e4799991ec Remove reference to non-existing css selector form-signin 2016-02-11 12:53:35 +01:00
Johannes Zellner 66167e74dc Cleanup the user forms 2016-02-11 12:50:02 +01:00
Johannes Zellner 5643d49bef Check if user deletion actually affected a row 2016-02-11 12:07:43 +01:00
Johannes Zellner 81ec26e45c Ensure we can delete users which belong to a group 2016-02-11 12:02:35 +01:00
Johannes Zellner 72c5ebcc06 Add user api tests for adding/removing from admin group 2016-02-11 11:39:19 +01:00
Johannes Zellner ecf7575dd3 UserError.NOT_ALLOWED is not unused 2016-02-11 11:32:48 +01:00
Johannes Zellner 98a7f44dc1 Check for last admin not required anymore
This is now prevented by the fact that an admin
cannot remove itself from the admin group. There
remains a race, just like before, where two admins could
trigger an admin group removal of the other admin in parallel
and the calls are in a state after admin flag check of
the used tokens. This can only be prevented with a db constraint
in the end.
2016-02-11 11:30:21 +01:00
Johannes Zellner 5fce9c8d1f Do not allow an admin remove itself from admins group 2016-02-11 11:29:04 +01:00
Johannes Zellner 0ea89fccb8 Remove admin api route tests 2016-02-11 11:26:35 +01:00
Johannes Zellner 2c2922d725 Make tests succeed for now
We still have to bring back the sending of email when admins are changed
2016-02-11 11:26:35 +01:00
Johannes Zellner fbeefeca7d setGroups() has no result 2016-02-11 11:26:35 +01:00
Johannes Zellner 163ceef527 Remove the admin toggle route 2016-02-11 11:26:35 +01:00
Girish Ramakrishnan db5cc1f694 dropping groupMembers table makes not much sense 2016-02-10 08:57:06 -08:00
Johannes Zellner a3b9a7365c No need to check for admin, the whole view is admin only 2016-02-10 17:35:40 +01:00
Johannes Zellner 213b2a2802 show groups of each user 2016-02-10 17:34:29 +01:00
Johannes Zellner 229d09bb9e Give the group buttons some space 2016-02-10 17:26:51 +01:00
Johannes Zellner f127680c8c Fixup the group delete dialog title 2016-02-10 17:23:42 +01:00
Johannes Zellner f767f7f1b9 We do not actually allow group edit 2016-02-10 17:22:04 +01:00
Johannes Zellner acb1afa955 Make delete action the last one 2016-02-10 17:20:24 +01:00
Johannes Zellner d132109925 Fixup the modal dialog css selector 2016-02-10 17:16:50 +01:00
Johannes Zellner 820e417026 angular requires special treatment for DELETE 2016-02-10 17:12:58 +01:00
Johannes Zellner 94bd0c606b Add group removal ui 2016-02-10 16:59:24 +01:00
Johannes Zellner 9a8328e6db We should really use camelCase 2016-02-10 16:43:23 +01:00
Johannes Zellner 5c75d64a07 Do not forget to reset the busy state 2016-02-10 16:41:32 +01:00
Johannes Zellner a8001995c8 Add business logic for group adding 2016-02-10 16:37:58 +01:00
Johannes Zellner 9ba4d52fb7 Add Client.createGroup() 2016-02-10 16:37:46 +01:00
Johannes Zellner 0e613a1cab Ensure focus 2016-02-10 16:25:38 +01:00
Johannes Zellner cf3d503a74 Add group add form 2016-02-10 16:24:25 +01:00
Johannes Zellner 1ab46a96f9 Reset the user edit form password error 2016-02-10 15:18:36 +01:00
Johannes Zellner 1a3164ef32 Add ui components for group management 2016-02-10 15:15:09 +01:00
Johannes Zellner bd62efcff5 That api is of course a PUT api 2016-02-10 15:01:51 +01:00
Johannes Zellner 7fc37b7c70 Allow admins to edit other users 2016-02-10 14:48:54 +01:00
Johannes Zellner 8ddccae15a Save the groups on user edit 2016-02-10 14:47:49 +01:00
Johannes Zellner 675d7c8730 Add Client.setGroups() 2016-02-10 14:47:35 +01:00
Johannes Zellner ba35d4a313 remove unused class 2016-02-10 14:40:29 +01:00
Johannes Zellner c1280ddcc2 Make group buttons toggle able 2016-02-10 14:39:49 +01:00
Johannes Zellner 36ded4c06a Group -> Groups 2016-02-10 14:26:17 +01:00
Johannes Zellner 9fb276019e Add ui elements for group selection 2016-02-10 14:26:04 +01:00
Johannes Zellner 19982b1815 Add Client.getGroups() 2016-02-10 14:25:08 +01:00
Johannes Zellner 459d5b8f60 Adjust user action buttons 2016-02-10 14:07:37 +01:00
Johannes Zellner 8ba5dc2352 Fix indentation 2016-02-10 13:55:49 +01:00
Johannes Zellner 8c73a7c7c2 Send admin flag with user profile 2016-02-10 13:35:16 +01:00
Johannes Zellner e78dd41e88 Replace deprecated gulp-minify-css with gulp-cssnano 2016-02-10 13:13:08 +01:00
Johannes Zellner 59ecb056d0 Fixup the oauth tests to set memoryLimit 2016-02-10 12:49:02 +01:00
Johannes Zellner 11b17fec3a Ensure groupMembers table is created first 2016-02-10 12:38:00 +01:00
Johannes Zellner 5ea81d0fd3 Ensure default minimum memory limit 2016-02-10 12:30:19 +01:00
Johannes Zellner 19cbd1f394 Ensure we never go below 256mb memoryLimit 2016-02-10 12:30:19 +01:00
Johannes Zellner 1b7265f866 Fixup the merge 2016-02-10 12:28:57 +01:00
Johannes Zellner 1cdb64e78d Use memoryLimit from app object instead of manifest in docker.js 2016-02-10 12:25:26 +01:00
Johannes Zellner eec8708249 Set memory limit on app installation to the default or the one specified in the manifest 2016-02-10 12:25:26 +01:00
Johannes Zellner ab003bf81f adjust appdb.js and unit tests to support memoryLimit 2016-02-10 12:25:26 +01:00
Johannes Zellner 2d60901b6e memoryLimit has to be BIGINT 2016-02-10 12:25:26 +01:00
Johannes Zellner 3fc9bde4f4 Make memoryLimit step in 8s 2016-02-10 12:25:26 +01:00
Johannes Zellner 4fc0df31fe Add apps.memoryLimit 2016-02-10 12:25:26 +01:00
Girish Ramakrishnan 3ac326e766 try upto 5 minutes to download the tarball
DO/S3 can be really slow at times
2016-02-09 21:07:03 -08:00
Girish Ramakrishnan 4770f9ddf6 add hasAccessTo tests 2016-02-09 21:07:03 -08:00
girish@cloudron.io 7e60fd554a add some failing groups for good measure 2016-02-09 18:55:42 -08:00
girish@cloudron.io c1cd7ac129 fix typo 2016-02-09 18:53:14 -08:00
girish@cloudron.io aab62263a7 add accessRestriction group test in oauth2 2016-02-09 18:52:27 -08:00
girish@cloudron.io 79889a0aac test simple auth accessRestriction 2016-02-09 18:40:20 -08:00
Girish Ramakrishnan f413bfb3a0 Add route to set the users groups 2016-02-09 16:43:32 -08:00
Girish Ramakrishnan 2b0791f4a3 rename vars 2016-02-09 16:19:00 -08:00
Girish Ramakrishnan d95339534f rename test file 2016-02-09 16:17:01 -08:00
Girish Ramakrishnan 82cf667f3b Add groups route tests 2016-02-09 15:26:46 -08:00
girish@cloudron.io e20b3f75e4 Handle NOT_EMPTY group deletion 2016-02-09 13:45:28 -08:00
girish@cloudron.io 6cca7b3e0e initial rest API for groups 2016-02-09 13:34:36 -08:00
girish@cloudron.io 0b814af206 Add api to get groups 2016-02-09 13:33:30 -08:00
girish@cloudron.io bfdabf9272 check groups property in accessRestriction 2016-02-09 13:03:52 -08:00
girish@cloudron.io 60988ff7f3 make hasAccessTo take a callback 2016-02-09 12:48:21 -08:00
girish@cloudron.io 3649fd0c31 drop the gid: prefix for group id
like the username, id and name is same in groups.
2016-02-09 12:28:50 -08:00
girish@cloudron.io 00c5aa041f clear database in backups test 2016-02-09 12:21:36 -08:00
girish@cloudron.io 4569b67007 make users and admins groups as reserved 2016-02-09 12:16:30 -08:00
girish@cloudron.io 1fb26bc441 make startAppTask take a callback 2016-02-09 12:14:04 -08:00
girish@cloudron.io e6d23a9701 stop previous task explicitly
there is a race:
1. task is running
2. new task is created overwriting the installationState
3. new task kills the old task of step 1. this results in installationState getting overwritten by 'error' because of the sigkill
4. new task that is launched loses the installationState that was step in 2.
2016-02-09 12:09:20 -08:00
girish@cloudron.io 0785266741 kill immediately. by default, it sends SIGTERM 2016-02-09 12:03:21 -08:00
girish@cloudron.io e752949752 make all tests work after group changes 2016-02-09 11:29:32 -08:00
girish@cloudron.io 199eb2b3e1 set the admin flag in user object 2016-02-09 09:25:17 -08:00
Girish Ramakrishnan 49cbea93fb fix ldap test 2016-02-09 08:52:16 -08:00
girish@cloudron.io 451c410547 make user test pass 2016-02-08 21:17:21 -08:00
girish@cloudron.io f6541720c4 pass owner flag in createUser 2016-02-08 21:05:02 -08:00
girish@cloudron.io 5e5435e869 send email for userAded 2016-02-08 20:51:20 -08:00
girish@cloudron.io 0d4f113d7d add groupIds to user object 2016-02-08 20:38:50 -08:00
girish@cloudron.io 14fab0992f make user test mostly work 2016-02-08 16:53:20 -08:00
girish@cloudron.io d7eb004bc1 remove admin arg from user.create 2016-02-08 16:36:45 -08:00
girish@cloudron.io c34f3ee653 null invitor is ok 2016-02-08 16:36:26 -08:00
girish@cloudron.io 96d595de39 fix database test 2016-02-08 16:25:29 -08:00
girish@cloudron.io b1f4508313 remove admin references from userdb 2016-02-08 16:18:51 -08:00
girish@cloudron.io 52ce59faaf createUser does not take admin anymore 2016-02-08 16:14:43 -08:00
girish@cloudron.io 85085ae0b2 implement getAllAdmins based on groups 2016-02-08 16:10:44 -08:00
girish@cloudron.io c14cf9c260 migrate admin flag to group membership 2016-02-08 16:07:44 -08:00
girish@cloudron.io a47c6f0774 make requires alphabetical 2016-02-08 15:17:54 -08:00
girish@cloudron.io 888955bd9b add groups.isMember 2016-02-08 10:53:01 -08:00
girish@cloudron.io 6abf5e2c44 group members: add/remove/get 2016-02-08 10:48:21 -08:00
girish@cloudron.io b1935c3550 restrict group names 2016-02-08 09:41:25 -08:00
girish@cloudron.io e39d7750c5 add group membership table 2016-02-08 08:55:37 -08:00
girish@cloudron.io 1d83a48a1a make group id distinct from name 2016-02-08 08:44:18 -08:00
Girish Ramakrishnan 802ee6c456 more group tests 2016-02-07 20:49:55 -08:00
Girish Ramakrishnan 278085ba22 initial tests for adding group 2016-02-07 20:34:05 -08:00
Girish Ramakrishnan b945a8a04c add groups model and db code 2016-02-07 20:25:08 -08:00
Girish Ramakrishnan 7ef92071c5 add groups table 2016-02-07 20:11:37 -08:00
girish@cloudron.io c16ab95193 reword CLI message 2016-02-04 23:13:29 -08:00
Girish Ramakrishnan c5e2d9a9cc download new app image as the first thing in update
this will reduce downtime.
2016-02-04 22:49:22 -08:00
Girish Ramakrishnan 07df76b25e Change developer mode text to cli 2016-02-04 22:49:22 -08:00
girish@cloudron.io 5b264565db set the target for cloudron links 2016-02-04 18:22:53 -08:00
girish@cloudron.io a3561bd040 Make Cloudron a link and fix copyright 2016-02-04 17:47:31 -08:00
girish@cloudron.io 6e4f47e807 0.8.1 changes 2016-02-04 15:20:40 -08:00
Johannes Zellner 471965dc66 Add initial still disabled slider to app configure 2016-02-04 16:45:00 +01:00
Johannes Zellner 3b109ea2e7 Include bootstrap-slider in our angular app 2016-02-04 16:44:40 +01:00
Johannes Zellner 6011526d5e Add bootstrap-slider assets 2016-02-04 16:44:03 +01:00
Johannes Zellner 1395d2971b Send app update changelog with email
Fixes #579
2016-02-04 15:31:40 +01:00
Johannes Zellner e9d6badae7 Changelog is just a flat text, no array 2016-02-04 15:17:18 +01:00
Johannes Zellner 65ddc7f24c Show changelog in app update ui 2016-02-04 15:17:18 +01:00
girish@cloudron.io fa871c7ada some apache configs require Host header to be set 2016-02-03 20:18:59 -08:00
girish@cloudron.io 8652d6c136 Add 0.8.0 changes 2016-02-02 08:51:13 -08:00
girish@cloudron.io 16d976a145 use multidb version of mysql addon 2016-02-02 08:46:09 -08:00
girish@cloudron.io fa1f5cc454 call the multi methods if multipleDatabases is set 2016-02-02 08:41:41 -08:00
Johannes Zellner 84c3b367d5 Actually wait for apps to be stopped 2016-01-29 17:24:18 +01:00
Johannes Zellner 793aa6512d Rework parts of the apps tests to be more reliable
No need to create and tear down the addons everytime.
Docker proxy can also be just injected once.
Wait explicitly for apptasks to be terminated.
2016-01-29 16:27:37 +01:00
Johannes Zellner 98ab99ab34 Add callback to config._reset() for convenience 2016-01-29 16:27:04 +01:00
Johannes Zellner 24a826bdd1 Reduce from 20 to 10 sec wait for addons
Since that is reliable already with 10 on my laptop
I assume this is fine for any other more modern machine
2016-01-29 16:26:15 +01:00
Johannes Zellner 05245f5fc7 Add a way to wait and stop pending tasks 2016-01-29 16:25:31 +01:00
Johannes Zellner b718c8d044 Cleanup config before and after apps tests 2016-01-29 14:30:40 +01:00
Johannes Zellner 2888a85081 Remove cloudron side migrate api
This is some old api when we had a migrate view in the webadmin
This is entirely handled on the appstore for now.
2016-01-29 14:17:33 +01:00
Johannes Zellner 307262244a Also cleanup the config file on config._reset() 2016-01-29 14:17:10 +01:00
Johannes Zellner 9a875634f8 Improve error message in mailer 2016-01-29 12:40:34 +01:00
Johannes Zellner 4af33486ae Add missing fi in shell script 2016-01-29 12:31:59 +01:00
Johannes Zellner befa898f18 Do not show app version, but make it available as a tooltip on the title for us 2016-01-29 12:29:35 +01:00
Johannes Zellner 18525e1236 Show app website in appstore install dialog
Fixes #580
2016-01-29 12:26:42 +01:00
Johannes Zellner 28ffd01cf4 Invoke BACKUP_APP_CMD with the additional backupConfig.url 2016-01-29 11:55:52 +01:00
Johannes Zellner 09c7aa4440 Remove whitespace 2016-01-29 11:54:58 +01:00
Johannes Zellner ea4862d351 Strictly assert on app 2016-01-29 11:54:40 +01:00
Johannes Zellner 3e4d62329e Also copy the backup config json 2016-01-29 11:54:15 +01:00
Johannes Zellner d12366576b Add backup config url to backupapp.sh 2016-01-29 11:44:24 +01:00
Johannes Zellner 7b1d906494 Add backups.getAppBackupConfigUrl() 2016-01-29 11:44:24 +01:00
Johannes Zellner 0972c88b8b Set the correct environment for the installer.sh 2016-01-28 16:36:34 +01:00
girish@cloudron.io 9464a26a7e put version in the mail 2016-01-27 10:35:48 -08:00
263 changed files with 25518 additions and 7979 deletions
-1
View File
@@ -1,6 +1,5 @@
node_modules/
coverage/
docs/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
+6 -5
View File
@@ -1,7 +1,8 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ]
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
+224
View File
@@ -400,3 +400,227 @@
- Improved box update management using prereleases
- Less aggressive disk space checks
[0.8.0]
- MySQL addon : multiple database support
[0.8.1]
- Set Host HTTP header when querying healthCheckPath
- Show application Changelog in app update emails
[0.9.0]
- Fix bug in multdb mysql addon backup
- Add initial user group support
- Improved app memory limit handling
[0.9.1]
- Introduce per app group access control
[0.9.2]
- Fix bug where reconfiguring apps would trigger memory limit warning
- Allow more apps to be installed in bigger sized cloudrons
- Allow user to override memory limit warning and install anyway
[0.9.3]
- Admin flag is handled outside of groups
- User interface fixes for groups
- Allow to set access restrictions on app installation
[0.10.0]
- Upgrade to docker 1.10.2
- Fix MySQL addon to handle heavier loads
- Allow listing and download of backups (using the CLI tool)
- Ubuntu security updates till 8th March 2016 (http://www.ubuntu.com/usn)
[0.10.1]
- Fix Let's Encrypt certificate renewal
[0.10.2]
- Apps can now bind with username or email with LDAP
- Disallow updating an app with mismatching manifest id
- Use admin domain instead of naked domain in the SPF record
- Download Lets Encrypt intermediate cert
[0.10.3]
- Store the backup config for each backup. This will allow using multiple buckets/providers for backups simultaneously.
- Fix SPF record check
[0.10.4]
- Fix restore for droplets in EU region
[0.11.0]
- Store backups in the same region as the Cloudron
- Fix PCRE security issue (http://www.ubuntu.com/usn/usn-2943-1/)
[0.11.1]
- Improve the backup logic
[0.11.2]
- Allow users to choose a username on first sign up
- Fix app graphs
[0.12.0]
- Fix upload of large backups
- Postgres addon whitelists pg_trgm and hstore extensions
- Suppress boring update emails from patch releases
- Setup bounce alerts for emails
- Query admin's name in activation wizard
- Admin emails are now delivered as no-reply
- Fix crash when user attempts to set a duplicate email
- Improved mongodb crash recovery
[0.12.1]
- Fix crash when backing up apps
[0.12.2]
- Improved error handling for addons
[0.12.3]
- LDAP: Do not set sn attribute when user has no surname
[0.12.4]
- Install app only after platform is ready
[0.12.5]
- Get alerts for app task failures
- Fix update issue when one or more apps are in failed state
[0.12.6]
- Allow setting an alternate external domain for apps
[0.12.7]
- Fix changing password
[0.13.0]
- Upgrade to ubuntu 16.04
- Add event log
[0.13.1]
- Make activity log viewable to admins
- Fix geoip lookup
[0.13.2]
- Fix crash in app auto updater
- Fix crash with empty timezone
[0.13.3]
- Enable auth in email addon
- Add search for activity log
- Add tutorial for first time users
[0.13.4]
- Fix mail addon restart issue
[0.14.0]
- You have mail :-)
[0.14.1]
- 2-character usernames are now allowed
- Make cloudron CLI push/pull more robust
[0.14.2]
- Update mail addon
[0.15.0]
- [REST API](https://cloudron.io/references/api.html) is now in public beta
- Enable Developer mode by default for new Cloudrons
- Reverse proxy fixes for apps exposing a WebDav server
- Allow admins to optionally set the username and displayName on user creation
- Fix app autoupdate logic to detect if one or more in-use port bindings was removed
[0.15.1]
- Fix mail connectivity from IPv6 clients
- Add API token management UI
- Improved UI to enter email aliases
[0.15.2]
- Allow restoring apps from any previous backup
[0.15.3]
- Show installation progress in a tooltip
[0.16.0]
- Allow apps to be configured in configuring state
- Improved platform architecture that allows incremental infrastructure updates
- Implement app clone
[0.16.1]
- Fix UI layout issue in tokens page
- Resume app tasks only when configured and platform ready
- Allow errored apps to be reconfigured
[0.16.2]
- Fix assert when backing up apps in errored state
- Fix bug where multiple redis installations caused an error
[0.16.3]
- Timeout in 10mins if app restore fails because of external domain CNAME setup
[0.16.4]
- Setup email aliases to only alias names for the Cloudron domain
[0.16.5]
- Allow sending email with alias as the From
[0.16.6]
- Add plan migration interface
- Initial EC2 support
[0.17.0]
- Public beta release of Cloudron Mail Server
- Add new DNS & Certs UI that enables easy migration to a custom domain
- Allow sending and receiving email from alias subaddresses
- Fix installation issue with some apps on the naked domain
[0.17.1]
- Preliminary user impersonation support
- Fix crash in mail container when generating bounces
[0.17.2]
- Add config option to embed apps in other sites
[0.17.3]
- Incremental infrastructure update logic
- Keep eventlogs only for a week
- Throttle OOM mails
[0.17.4]
- Add warning for users moving to custom domains
- Out of disk space and certificate renewal mails are now sent to cloudron owner for selfhosters
- Fix a bug where selfhosted Cloudrons do not start because of a MySQL error
- Implement new app versioning & update scheme
[0.17.5]
- Fix migration interface issue
- Allow self hosted Cloudron to login to the Cloudron Store
- Send mail to self hosted Cloudron admins about OOM and App died errors
- Fix bug where box update emails are sent repeatedly
[0.18.0]
- Fix app bundle installation
- Fix RBL lookup in mail server
- Add spam filter for email
[0.19.0]
* New base image 0.19.0
* Upgrade PostgreSQL and MySQL
[0.19.1]
* Make email optional (settings -> enable/disable mail)
* Make PostgresSQL behave better in low memory cloudrons
* Add demo mode check
* Fix plan listing
[0.20.0]
* Fix bug where crash reports where not being sent to support@cloudron.io (#29)
* Do not overwrite existing DNS records during app installation (#27)
* Add UI to configure app's memory limit (#18)
* Fix OAuth proxy support (#6)
[0.20.1]
* Fix bug where oauth proxy was installed for apps with customAuth
[0.20.2]
* Fix memory limit slider to start from the minimum memory (#43)
* Save user certs separately from automatic certs (#44)
* Fix access control display for email apps (#45)
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016 yellowtent
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
+69 -11
View File
@@ -1,17 +1,75 @@
Cloudron a Smart Server
=======================
# Cloudron
[Cloudron](https://cloudron.io) is the best way to run apps on your server.
Web applications like email, contacts, blog, chat are the backbone of the modern
internet. Yet, we live in a world where hosting these essential applications is
a complex task.
Selfhost Instructions
---------------------
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
anyone to effortlessly host web applications on their server on their own terms.
The smart server currently relies on an AWS account with access to Route53 and S3 and is tested on DigitalOcean and EC2.
## Features
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
* Per-app encrypted backups and restores.
* App updates delivered via the App Store.
* Secure - Cloudron manages the firewall. All apps are secured with HTTPS. Certificates are
installed and renewed automatically.
* Centralized User & Group management. Control who can access which app.
* Single Sign On. Use same credentials across all apps.
* Automatic updates for the Cloudron platform.
* Trivially migrate to another server keeping your apps and data (for example, switch your
infrastructure provider or move to a bigger server).
* Comprehensive [REST API](https://cloudron.io/references/api.html).
* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps.
* Alerts, audit logs, graphs, dns management ... and much more
## Demo
Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudron).
## Installing
You can install the Cloudron platform on your own server or get a managed server
from cloudron.io.
* [Selfhosting](https://cloudron.io/references/selfhosting.html)
* [Managed Hosting](https://cloudron.io/pricing.html)
## Documentation
* [User manual](https://cloudron.io/references/usermanual.html)
* [Developer docs](https://cloudron.io/documentation.html)
* [Architecture](https://cloudron.io/references/architecture.html)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
The [graphite repo](https://git.cloudron.io/cloudron/docker-graphite) contains the graphite code
that collects metrics for graphs.
The addons are located in separate repositories
* [Redis](https://git.cloudron.io/cloudron/redis-addon)
* [Postgresql](https://git.cloudron.io/cloudron/postgresql-addon)
* [MySQL](https://git.cloudron.io/cloudron/mysql-addon)
* [Mongodb](https://git.cloudron.io/cloudron/mongodb-addon)
* [Mail](https://git.cloudron.io/cloudron/mail-addon)
## Community
* [Chat](https://chat.cloudron.io/)
* [Support](mailto:support@cloudron.io)
```
curl https://s3.amazonaws.com/prod-cloudron-releases/installer.sh -o installer.sh
chmod +x installer.sh
./installer.sh <domain> <aws access key> <aws acccess secret> <backup bucket> <provider> <release sha1>
```
@@ -10,7 +10,6 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
provider="digitalocean"
installer_revision=$(git rev-parse HEAD)
box_name=""
server_id=""
@@ -23,14 +22,13 @@ deploy_env="dev"
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "provider:,revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--env) deploy_env="$2"; shift 2;;
--revision) installer_revision="$2"; shift 2;;
--provider) provider="$2"; shift 2;;
--name) box_name="$2"; destroy_server="no"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--) break;;
@@ -38,28 +36,23 @@ while true; do
esac
done
echo "Creating image using ${provider}"
if [[ "${provider}" == "digitalocean" ]]; then
if [[ "${deploy_env}" == "staging" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
elif [[ "${deploy_env}" == "dev" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
elif [[ "${deploy_env}" == "prod" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
else
echo "No such env ${deploy_env}."
exit 1
fi
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
echo "Creating digitalocean image"
if [[ "${deploy_env}" == "staging" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
elif [[ "${deploy_env}" == "dev" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
elif [[ "${deploy_env}" == "prod" ]]; then
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
else
echo "Unknown provider : ${provider}"
echo "No such env ${deploy_env}."
exit 1
fi
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
@@ -137,15 +130,15 @@ while true; do
sleep 30
done
echo "Copying INFRA_VERSION"
$scp22 "${SCRIPT_DIR}/../setup/INFRA_VERSION" root@${server_ip}:.
echo "Copying infra_version.js"
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
echo "Executing init script"
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh ${installer_revision}"; then
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh ${installer_revision} caas"; then
echo "Init script failed"
exit 1
fi
+192
View File
@@ -0,0 +1,192 @@
#!/bin/bash
set -eu -o pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
installer_revision=$(git rev-parse HEAD)
instance_id=""
server_ip=""
destroy_server="yes"
ami_id="ami-f9e30f96"
region="eu-central-1"
aws_credentials="baseimage"
security_group="sg-b9a473d1"
instance_type="t2.small"
subnet_id="subnet-801402e9"
key_pair_name="id_rsa_yellowtent"
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
# brew install gnu-getopt to get the GNU getopt on OS X
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "revisio0n:,no-destroy" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--revision) installer_revision="$2"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
if [[ ! -f "${ssh_keys}" ]]; then
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
exit 1
fi
function debug() {
echo "$@" >&2
}
function get_pretty_revision() {
local git_rev="$1"
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
echo "${sha1}"
}
now=$(date "+%Y-%m-%d-%H%M%S")
pretty_revision=$(get_pretty_revision "${installer_revision}")
echo "Creating EC2 instance"
instance_id=$(aws ec2 run-instances --image-id ${ami_id} --region ${region} --profile ${aws_credentials} --security-group-ids ${security_group} --instance-type ${instance_type} --key-name ${key_pair_name} --subnet-id ${subnet_id} --associate-public-ip-address | $JSON Instances[0].InstanceId)
echo "Got InstanceId: ${instance_id}"
# name the instance
aws ec2 create-tags --profile ${aws_credentials} --resources ${instance_id} --tags "Key=Name,Value=baseimage-${pretty_revision}"
echo "Waiting for instance to be running..."
while true; do
event_status=`aws ec2 describe-instances --instance-id ${instance_id} --region ${region} --profile ${aws_credentials} | $JSON Reservations[0].Instances[0].State.Name`
if [[ "${event_status}" == "running" ]]; then
break
fi
debug -n "."
sleep 10
done
server_ip=$(aws ec2 describe-instances --instance-id ${instance_id} --region ${region} --profile ${aws_credentials} | $JSON Reservations[0].Instances[0].PublicIpAddress)
echo "Server IP is: ${server_ip}"
while true; do
echo "Trying to copy init script to server"
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" ubuntu@${server_ip}:.; then
break
fi
echo "Timedout, trying again in 30 seconds"
sleep 30
done
echo "Copying infra_version.js"
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" ubuntu@${server_ip}:.
echo "Copying box source"
cd "${SOURCE_DIR}"
git archive --format=tar HEAD | $ssh22 "ubuntu@${server_ip}" "cat - > /tmp/box.tar.gz"
echo "Enabling root ssh access"
if ! $ssh22 "ubuntu@${server_ip}" "sudo sed -e 's/.* \(ssh-rsa.*\)/\1/' -i /root/.ssh/authorized_keys"; then
echo "Unable to enable root access"
echo "Make sure to cleanup the ec2 instance ${instance_id}"
exit 1
fi
echo "Executing init script"
if ! $ssh22 "root@${server_ip}" "/bin/bash /home/ubuntu/initializeBaseUbuntuImage.sh ${installer_revision} ec2"; then
echo "Init script failed"
echo "Make sure to cleanup the ec2 instance ${instance_id}"
exit 1
fi
echo "Strip ssh key"
if ! $ssh202 "root@${server_ip}" "rm /root/.ssh/authorized_keys"; then
echo "Unable to remove ssh access"
echo "Make sure to cleanup the ec2 instance ${instance_id}"
exit 1
fi
snapshot_name="cloudron-${pretty_revision}-${now}"
echo "Creating ami image ${snapshot_name}"
image_id=$(aws ec2 create-image --region ${region} --profile ${aws_credentials} --instance-id ${instance_id} --name ${snapshot_name} | $JSON ImageId)
echo "Image creation started for image id: ${image_id}"
echo "Waiting for image creation to finish..."
while true; do
event_status=`aws ec2 describe-images --region ${region} --profile ${aws_credentials} --image-id ${image_id} | $JSON Images[0].State`
if [[ "${event_status}" == "available" ]]; then
break
fi
debug -n "."
sleep 10
done
echo "Terminating instance"
aws ec2 terminate-instances --region ${region} --profile ${aws_credentials} --instance-ids ${instance_id}
echo "Make image public"
aws ec2 modify-image-attribute --region ${region} --profile ${aws_credentials} --image-id ${image_id} --launch-permission "{\"Add\":[{\"Group\":\"all\"}]}"
# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region
# Images are currently created in eu-central-1
echo "Coping image to other regions"
ec2_regions=( "us-east-1" "us-west-1" "us-west-2" "ap-south-1" "ap-northeast-2" "ap-southeast-1" "ap-southeast-2" "ap-northeast-1" "eu-west-1" "sa-east-1" )
ec2_amis=( )
for r in ${ec2_regions[@]}; do
echo "=> ${r}"
ami_id=$(aws ec2 copy-image --region ${r} --profile ${aws_credentials} --source-image-id ${image_id} --source-region ${region} --name ${snapshot_name} | $JSON ImageId)
# append in the same order as the regions
ec2_amis+=( ${ami_id} )
done
# wait for all images to be available
echo "Waiting for images to be ready (first will take the longest)..."
region_string="${region}=${image_id}"
i=0
while [ $i -lt ${#ec2_regions[*]} ]; do
echo "=> ${ec2_regions[$i]} ${ec2_amis[$i]}"
while true; do
event_status=`aws ec2 describe-images --region ${ec2_regions[$i]} --profile ${aws_credentials} --image-id ${ec2_amis[$i]} | $JSON Images[0].State`
if [[ "${event_status}" == "available" ]]; then
echo "done"
break
fi
debug -n "."
sleep 10
done
# now make it public
aws ec2 modify-image-attribute --region ${ec2_regions[$i]} --profile ${aws_credentials} --image-id ${ec2_amis[$i]} --launch-permission "{\"Add\":[{\"Group\":\"all\"}]}"
# append to output string for release tool
region_string+=",${ec2_regions[$i]}=${ec2_amis[$i]}"
# inc the iteration counter
i=$(( $i + 1));
done
echo ""
echo "--------------------------------------------------"
echo "New image id is: ${image_id}"
echo "Image region string for release:"
echo "${region_string}"
echo "--------------------------------------------------"
echo ""
+35 -14
View File
@@ -10,7 +10,7 @@ if [[ -z "${JSON}" ]]; then
exit 1
fi
readonly CURL="curl -s -u ${DIGITAL_OCEAN_TOKEN}:"
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
function debug() {
echo "$@" >&2
@@ -30,7 +30,7 @@ function create_droplet() {
local box_name="$2"
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-15-10-x64"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="512mb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
@@ -49,9 +49,9 @@ function get_droplet_ip() {
function get_droplet_id() {
local droplet_name="$1"
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=100" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=200" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
[[ -z "$id" ]] && exit 1
echo "$id"
echo "$id"
}
function power_off_droplet() {
@@ -109,13 +109,24 @@ function get_image_id() {
local snapshot_name="$1"
local image_id=""
image_id=$($CURL "https://api.digitalocean.com/v2/images?per_page=100" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id)
if [[ -n "${image_id}" ]]; then
echo "${image_id}"
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=200"); then
echo "Failed to get image listing. ${response}"
return 1
fi
if ! image_id=$(echo "$response" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
echo "Failed to parse curl response: ${response}"
return 1
fi
if [[ -z "${image_id}" ]]; then
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
return 1
fi
echo "${image_id}"
}
function snapshot_droplet() {
@@ -128,16 +139,26 @@ function snapshot_droplet() {
debug -n "Waiting for snapshot to complete"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
echo "Could not get action status. ${response}"
continue
fi
if ! event_status=$(echo "${response}" | $JSON action.status); then
echo "Could not parse action.status from response. ${response}"
continue
fi
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
debug "! done"
get_image_id "${snapshot_name}"
if ! image_id=$(get_image_id "${snapshot_name}"); then
return 1
fi
echo "${image_id}"
}
function destroy_droplet() {
@@ -177,7 +198,7 @@ function transfer_image_to_all_regions() {
local image_id="$1"
xfer_events=()
image_regions=(ams3) ## sfo1 is where the image is created
image_regions=(ams2) ## sfo1 is where the image is created
for image_region in ${image_regions[@]}; do
xfer_event=$(transfer_image ${image_id} ${image_region})
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
+61 -84
View File
@@ -6,7 +6,7 @@ readonly USER=yellowtent
readonly USER_HOME="/home/${USER}"
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
readonly INSTALLER_REVISION="$1"
readonly SELFHOSTED=$(( $# > 1 ? 1 : 0 ))
readonly PROVIDER="$2"
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
@@ -17,19 +17,7 @@ function die {
exit 1
}
[[ "$(systemd --version 2>&1)" == *"systemd 225"* ]] || die "Expecting systemd to be 225"
if [ -f "${SOURCE_DIR}/INFRA_VERSION" ]; then
source "${SOURCE_DIR}/INFRA_VERSION"
else
echo "No INFRA_VERSION found, skip pulling docker images"
fi
if [ ${SELFHOSTED} == 0 ]; then
echo "!! Initializing Ubuntu image for CaaS"
else
echo "!! Initializing Ubuntu image for Selfhosting"
fi
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
echo "==== Create User ${USER} ===="
if ! id "${USER}"; then
@@ -49,7 +37,7 @@ export DEBIAN_FRONTEND=noninteractive
echo "=== Upgrade ==="
apt-get update
apt-get upgrade -y
apt-get dist-upgrade -y
apt-get install -y curl
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
@@ -66,15 +54,11 @@ iptables -P OUTPUT ACCEPT
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
if [ ${SELFHOSTED} == 0 ]; then
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,202,443,886 -j ACCEPT
else
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,22,443,886 -j ACCEPT
fi
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A INPUT -s 172.17.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
iptables -A INPUT -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
# loopback
iptables -A INPUT -i lo -j ACCEPT
@@ -94,7 +78,9 @@ apt-get -y install btrfs-tools
echo "==== Install docker ===="
# install docker from binary to pin it to a specific version. the current debian repo does not allow pinning
curl https://get.docker.com/builds/Linux/x86_64/docker-1.9.1 > /usr/bin/docker
# IMPORTANT: docker 1.11.x breaks the --dns option hack that we use below
curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.2 > /usr/bin/docker
apt-get -y install aufs-tools
chmod +x /usr/bin/docker
groupadd docker
cat > /etc/systemd/system/docker.socket <<EOF
@@ -118,7 +104,7 @@ After=network.target docker.socket
Requires=docker.socket
[Service]
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --dns 127.0.0.1
MountFlags=slave
LimitNOFILE=1048576
LimitNPROC=1048576
@@ -129,10 +115,10 @@ WantedBy=multi-user.target
EOF
echo "=== Setup btrfs data ==="
fallocate -l "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
echo "${USER_DATA_FILE} ${USER_DATA_DIR} btrfs loop,nosuid 0 0" >> /etc/fstab
mkdir -p "${USER_DATA_DIR}" && mount "${USER_DATA_FILE}"
mkdir -p "${USER_DATA_DIR}"
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
systemctl daemon-reload
systemctl enable docker
@@ -149,40 +135,40 @@ iptables -I FORWARD -d 169.254.169.254 -j DROP
mkdir /etc/iptables && iptables-save > /etc/iptables/rules.v4
echo "=== Enable memory accounting =="
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
if [[ "${PROVIDER}" == "digitalocean" ]] || [[ "${PROVIDER}" == "caas" ]]; then
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="console=tty1 root=LABEL=DOROOT notsc clocksource=kvm-clock net.ifnames=0 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
else
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
fi
update-grub
# now add the user to the docker group
usermod "${USER}" -a -G docker
if [ -z $(echo "${INFRA_VERSION}") ]; then
echo "Skip pulling base docker images"
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "==== Downloading docker images ===="
if [ -f ${SOURCE_DIR}/infra_version.js ]; then
images=$(node -e "var i = require('${SOURCE_DIR}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo "Pulling images: ${images}"
for image in ${images}; do
docker pull "${image}"
done
else
echo "=== Pulling base docker images ==="
docker pull "${BASE_IMAGE}"
echo "=== Pulling mysql addon image ==="
docker pull "${MYSQL_IMAGE}"
echo "=== Pulling postgresql addon image ==="
docker pull "${POSTGRESQL_IMAGE}"
echo "=== Pulling redis addon image ==="
docker pull "${REDIS_IMAGE}"
echo "=== Pulling mongodb addon image ==="
docker pull "${MONGODB_IMAGE}"
echo "=== Pulling graphite docker images ==="
docker pull "${GRAPHITE_IMAGE}"
echo "=== Pulling mail relay ==="
docker pull "${MAIL_IMAGE}"
echo "No infra_versions.js found, skipping image download"
fi
echo "==== Install nginx ===="
apt-get -y install nginx-full
[[ "$(nginx -v 2>&1)" == *"nginx/1.9."* ]] || die "Expecting nginx version to be 1.9.x"
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
echo "==== Install build-essential ===="
apt-get -y install build-essential rcconf
@@ -190,11 +176,11 @@ apt-get -y install build-essential rcconf
echo "==== Install mysql ===="
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
apt-get -y install mysql-server
[[ "$(mysqld --version 2>&1)" == *"5.6."* ]] || die "Expecting nginx version to be 5.6.x"
apt-get -y install mysql-server-5.7
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
echo "==== Install pwgen ===="
apt-get -y install pwgen
echo "==== Install pwgen and swaks awscli ===="
apt-get -y install pwgen swaks awscli
echo "==== Install collectd ==="
if ! apt-get install -y collectd collectd-utils; then
@@ -209,35 +195,22 @@ echo "==== Install logrotate ==="
apt-get install -y cron logrotate
systemctl enable cron
echo "==== Install nodejs ===="
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
mkdir -p /usr/local/node-4.1.1
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
ln -s /usr/local/node-4.1.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
echo "=== Rebuilding npm packages ==="
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
cd "${INSTALLER_SOURCE_DIR}" && while ! npm install --production; do sleep 1; done
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
echo "==== Install installer systemd script ===="
provisionEnv="PROVISION=digitalocean"
if [ ${SELFHOSTED} == 1 ]; then
provisionEnv="PROVISION=local"
fi
cat > /etc/systemd/system/cloudron-installer.service <<EOF
[Unit]
Description=Cloudron Installer
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
After=box-setup.service
[Service]
Type=idle
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
Environment="DEBUG=installer*,connect-lastmile"
; kill any child (installer.sh) as well
KillMode=control-group
Restart=on-failure
@@ -264,12 +237,13 @@ EOF
# Allocate swap files
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
# On ubuntu ec2 we use cloud-init https://wiki.archlinux.org/index.php/Cloud-init
echo "==== Install box-setup systemd script ===="
cat > /etc/systemd/system/box-setup.service <<EOF
[Unit]
Description=Box Setup
Before=docker.service umount.target collectd.service
After=do-resize.service
Before=docker.service collectd.service mysql.service sshd.service nginx.service
After=cloud-init.service
[Service]
Type=oneshot
@@ -310,16 +284,19 @@ chown root:systemd-journal /var/log/journal
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
if [ ${SELFHOSTED} == 0 ]; then
echo "==== Install ssh ==="
apt-get -y install openssh-server
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?Port .*/Port 202/g' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-i /etc/ssh/sshd_config
echo "==== Install ssh ==="
apt-get -y install openssh-server
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?Port .*/Port 202/g' \
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
echo "==== Install unbound DNS ==="
apt-get -y install unbound
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
-42
View File
@@ -1,42 +0,0 @@
#!/usr/bin/env node
'use strict';
var assert = require('assert'),
mailer = require('./src/mailer.js'),
safe = require('safetydance'),
path = require('path'),
util = require('util');
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
function collectLogs(program, callback) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
callback(null, logs);
}
function sendCrashNotification(processName) {
collectLogs(processName, function (error, result) {
if (error) {
console.error('Failed to collect logs.', error);
result = util.format('Failed to collect logs.', error);
}
console.log('Sending crash notification email for', processName);
mailer.sendCrashNotification(processName, result);
});
}
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);
sendCrashNotification(processName);
}
main();
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env node
'use strict';
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
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);
sendFailureLogs(processName, { unit: processName });
}
main();
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
set -eu
./node_modules/.bin/apidoc -i src/routes -o docs
+386
View File
@@ -0,0 +1,386 @@
# Addons
## Overview
Addons are services like database, authentication, email, caching that are part of the
Cloudron runtime. Setup, provisioning, scaling and maintanence of addons is taken care of
by the runtime.
The fundamental idea behind addons is to allow sharing of Cloudron resources across applications.
For example, a single MySQL server instance can be used across multiple apps. The Cloudron
runtime sets up addons in such a way that apps are isolated from each other.
## Using Addons
Addons are opt-in and must be specified in the [Cloudron Manifest](/references/manifest.html).
When the app runs, environment variables contain the necessary information to access the addon.
For example, the mysql addon sets the `MYSQL_URL` environment variable which is the
connection string that can be used to connect to the database.
When working with addons, developers need to remember the following:
* Environment variables are subject to change every time the app restarts. This can happen if the
Cloudron is rebooted or restored or the app crashes or an addon is re-provisioned. For this reason,
applications must never cache the value of environment variables across restarts.
* Addons must be setup or updated on each application start up. Most applications use DB migration frameworks
for this purpose to setup and update the DB schema.
* Addons are configured in the [addons section](/references/manifest.html#addons) of the manifest as below:
```
{
...
"addons": {
"oauth": { },
"redis" : { }
}
}
```
## All addons
### email
This addon allows an app to send and recieve emails on behalf of the user. The intended use case is webmail applications.
If an app wants to send mail (e.g notifications), it must use the [sendmail](/references/addons#sendmail)
addon. If the app wants to receive email (e.g user replying to notification), it must use the
[recvmail](/references/addons#recvmail) addon instead.
Apps using the IMAP and ManageSieve services below must be prepared to accept self-signed certificates (this is not a problem
because these are addresses internal to the Cloudron).
Exported environment variables:
```
MAIL_SMTP_SERVER= # SMTP server IP or hostname. Supports STARTTLS (TLS upgrade is enforced).
MAIL_SMTP_PORT= # SMTP server port
MAIL_IMAP_SERVER= # IMAP server IP or hostname. TLS required.
MAIL_IMAP_PORT= # IMAP server port
MAIL_SIEVE_SERVER= # ManageSieve server IP or hostname. TLS required.
MAIL_SIEVE_PORT= # ManageSieve server port
MAIL_DOMAIN= # Domain of the mail server
```
### ldap
This addon provides LDAP based authentication via LDAP version 3.
Exported environment variables:
```
LDAP_SERVER= # ldap server IP
LDAP_PORT= # ldap server port
LDAP_URL= # ldap url of the form ldap://ip:port
LDAP_USERS_BASE_DN= # ldap users base dn of the form ou=users,dc=cloudron
LDAP_GROUPS_BASE_DN= # ldap groups base dn of the form ou=groups,dc=cloudron
LDAP_BIND_DN= # DN to perform LDAP requests
LDAP_BIND_PASSWORD= # Password to perform LDAP requests
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `ldapsearch` client within the context of the app:
```
cloudron exec
# list users
> ldapsearch -x -h "${LDAP_SERVER}" -p "${LDAP_PORT}" -b "${LDAP_USERS_BASE_DN}"
# list users with authentication (Substitute username and password below)
> ldapsearch -x -D cn=<username>,${LDAP_USERS_BASE_DN} -w <password> -h "${LDAP_SERVER}" -p "${LDAP_PORT}" -b "${LDAP_USERS_BASE_DN}"
# list admins
> ldapsearch -x -h "${LDAP_SERVER}" -p "${LDAP_PORT}" -b "${LDAP_USERS_BASE_DN}" "memberof=cn=admins,${LDAP_GROUPS_BASE_DN}"
# list groups
> ldapsearch -x -h "${LDAP_SERVER}" -p "${LDAP_PORT}" -b "${LDAP_GROUPS_BASE_DN}"
```
### localstorage
Since all Cloudron apps run within a read-only filesystem, this addon provides a writeable folder under `/app/data/`.
All contents in that folder are included in the backup. On first run, this folder will be empty. File added in this path
as part of the app's image (Dockerfile) won't be present. A common pattern is to create the directory structure required
the app as part of the app's startup script.
The permissions and ownership of data within that directory are not guranteed to be preserved. For this reason, each app
has to restore permissions as required by the app as part of the app's startup script.
If the app is running under the recommeneded `cloudron` user, this can be achieved with:
```
chown -R cloudron:cloudron /app/data
```
### mongodb
By default, this addon provide mongodb 2.6.3.
Exported environment variables:
```
MONGODB_URL= # mongodb url
MONGODB_USERNAME= # username
MONGODB_PASSWORD= # password
MONGODB_HOST= # server IP/hostname
MONGODB_PORT= # server port
MONGODB_DATABASE= # database name
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `mongo` shell within the context of the app:
```
cloudron exec
# mongo -u "${MONGODB_USERNAME}" -p "${MONGODB_PASSWORD}" ${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DATABASE}
```
### mysql
By default, this addon provides a single database on MySQL 5.6.19. The database is already created and the application
only needs to create the tables.
Exported environment variables:
```
MYSQL_URL= # the mysql url (only set when using a single database, see below)
MYSQL_USERNAME= # username
MYSQL_PASSWORD= # password
MYSQL_HOST= # server IP/hostname
MYSQL_PORT= # server port
MYSQL_DATABASE= # database name (only set when using a single database, see below)
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `mysql` client within the context of the app:
```
cloudron exec
> mysql --user=${MYSQL_USERNAME} --password=${MYSQL_PASSWORD} --host=${MYSQL_HOST} ${MYSQL_DATABASE}
```
The `multipleDatabases` option can be set to `true` if the app requires more than one database. When enabled,
the following environment variables are injected:
```
MYSQL_DATABASE_PREFIX= # prefix to use to create databases
```
### oauth
The Cloudron OAuth 2.0 provider can be used in an app to implement Single Sign-On.
Exported environment variables:
```
OAUTH_CLIENT_ID= # client id
OAUTH_CLIENT_SECRET= # client secret
```
The callback url required for the OAuth transaction can be contructed from the environment variables below:
```
APP_DOMAIN= # hostname of the app
APP_ORIGIN= # origin of the app of the form https://domain
API_ORIGIN= # origin of the OAuth provider of the form https://my-cloudrondomain
```
OAuth2 URLs can be constructed as follows:
```
AuthorizationURL = ${API_ORIGIN}/api/v1/oauth/dialog/authorize # see above for API_ORIGIN
TokenURL = ${API_ORIGIN}/api/v1/oauth/token
```
The token obtained via OAuth has a restricted scope wherein they can only access the [profile API](/references/api.html#profile). This restriction
is so that apps cannot make undesired changes to the user's Cloudron.
We currently provide OAuth2 integration for Ruby [omniauth](https://github.com/cloudron-io/omniauth-cloudron) and Node.js [passport](https://github.com/cloudron-io/passport-cloudron).
### postgresql
By default, this addon provides PostgreSQL 9.4.4.
Exported environment variables:
```
POSTGRESQL_URL= # the postgresql url
POSTGRESQL_USERNAME= # username
POSTGRESQL_PASSWORD= # password
POSTGRESQL_HOST= # server name
POSTGRESQL_PORT= # server port
POSTGRESQL_DATABASE= # database name
```
The postgresql addon whitelists the hstore and pg_trgm extensions to be installable by the database owner.
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `psql` client within the context of the app:
```
cloudron exec
> PGPASSWORD=${POSTGRESQL_PASSWORD} psql -h ${POSTGRESQL_HOST} -p ${POSTGRESQL_PORT} -U ${POSTGRESQL_USERNAME} -d ${POSTGRESQL_DATABASE}
```
### recvmail
The recvmail addon can be used to receive email for the application.
Exported environment variables:
```
MAIL_IMAP_SERVER= # the IMAP server. this can be an IP or DNS name
MAIL_IMAP_PORT= # the IMAP server port
MAIL_IMAP_USERNAME= # the username to use for authentication
MAIL_IMAP_PASSWORD= # the password to use for authentication
MAIL_TO= # the to address to use
MAIL_DOMAIN= # the mail for which email will be received
```
The IMAP server only accepts TLS connections. The app must be prepared to accept self-signed certs (this is not a problem because the
imap address is internal to the Cloudron).
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `openssl` tool within the context of the app:
```
cloudron exec
> openssl s_client -connect "${MAIL_IMAP_SERVER}:${MAIL_IMAP_PORT}" -crlf
```
The IMAP command `? LOGIN username password` can then be used to test the authentication.
### redis
By default, this addon provides redis 2.8.13. The redis is configured to be persistent and data is preserved across updates
and restarts.
Exported environment variables:
```
REDIS_URL= # the redis url
REDIS_HOST= # server name
REDIS_PORT= # server port
REDIS_PASSWORD= # password
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `redis-cli` client within the context of the app:
```
cloudron exec
> redis-cli -h "${REDIS_HOST}" -p "${REDIS_PORT}" -a "${REDIS_PASSWORD}"
```
### scheduler
The scheduler addon can be used to run tasks at periodic intervals (cron).
Scheduler can be configured as below:
```
"scheduler": {
"update_feeds": {
"schedule": "*/5 * * * *",
"command": "/app/code/update_feed.sh"
}
}
```
In the above example, `update_feeds` is the name of the task and is an arbitrary string.
`schedule` values must fall within the following ranges:
* Minutes: 0-59
* Hours: 0-23
* Day of Month: 1-31
* Months: 0-11
* Day of Week: 0-6
_NOTE_: scheduler does not support seconds
`schedule` supports ranges (like standard cron):
* Asterisk. E.g. *
* Ranges. E.g. 1-3,5
* Steps. E.g. */2
`command` is executed through a shell (sh -c). The command runs in the same launch environment
as the application. Environment variables, volumes (`/tmp` and `/run`) are all
shared with the main application.
If a task is still running when a new instance of the task is scheduled to be started, the previous
task instance is killed.
### sendmail
The sendmail addon can be used to send email from the application.
Exported environment variables:
```
MAIL_SMTP_SERVER= # the mail server (relay) that apps can use. this can be an IP or DNS name
MAIL_SMTP_PORT= # the mail server port
MAIL_SMTP_USERNAME= # the username to use for authentication as well as the `from` username when sending emails
MAIL_SMTP_PASSWORD= # the password to use for authentication
MAIL_FROM= # the from address to use
MAIL_DOMAIN= # the domain name to use for email sending (i.e username@domain)
```
The SMTP server does not require STARTTLS. If STARTTLS is used, the app must be prepared to accept self-signed certs.
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `swaks` tool within the context of the app:
```
cloudron exec
> swaks --server "${MAIL_SMTP_SERVER}" -p "${MAIL_SMTP_PORT}" --from "${MAIL_SMTP_USERNAME}@${MAIL_DOMAIN}" --body "Test mail from cloudron app at $(hostname -f)" --auth-user "${MAIL_SMTP_USERNAME}" --auth-password "${MAIL_SMTP_PASSWORD}"
```
### simpleauth
Simple Auth can be used for authenticating users with a HTTP request. This method of authentication is targeted
at applications, which for whatever reason can't use the ldap addon.
The response contains an `accessToken` which can then be used to access the [Cloudron API](/references/api.html).
Exported environment variables:
```
SIMPLE_AUTH_SERVER= # the simple auth HTTP server
SIMPLE_AUTH_PORT= # the simple auth server port
SIMPLE_AUTH_URL= # the simple auth server URL. same as "http://SIMPLE_AUTH_SERVER:SIMPLE_AUTH_PORT
SIMPLE_AUTH_CLIENT_ID # a client id for identifying the request originator with the auth server
```
This addons provides two REST APIs:
**POST /api/v1/login**
Request JSON body:
```
{
"username": "<username> or <email>",
"password": "<password>"
}
```
Response 200 with JSON body:
```
{
"accessToken": "<accessToken>",
"user": {
"id": "<userId>",
"username": "<username>",
"email": "<email>",
"admin": <admin boolean>,
"displayName": "<display name>"
}
}
```
**GET /api/v1/logout**
Request params:
```
?access_token=<accessToken>
```
Response 200 with JSON body:
```
{}
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `curl` tool within the context of the app:
```
cloudron exec
> USERNAME=<enter username>
> PASSWORD=<enter password>
> PAYLOAD="{\"clientId\":\"${SIMPLE_AUTH_CLIENT_ID}\", \"username\":\"${USERNAME}\", \"password\":\"${PASSWORD}\"}"
> curl -H "Content-Type: application/json" -X POST -d "${PAYLOAD}" "${SIMPLE_AUTH_ORIGIN}/api/v1/login"
```
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
# Architecture
## Introduction
The Cloudron platform is designed to easily install and run web applications.
The application architecture is designed to let the Cloudron take care of system
operations like updates, backups, firewalls, domain management, certificate management
etc. This allows app developers to focus on their application logic instead of deployment.
At a high level, an application provides an `image` and a `manifest`. The image is simply
a docker image that is a bundle of the application code and it's dependencies. The manifest
file specifies application runtime requirements like database type and authentication scheme.
It also provides meta information for display purposes in the [Cloudron Store](/appstore.html)
like the title, icon and pricing.
Web applications like blogs, wikis, password managers, code hosting, document editing,
file syncers, notes, email, forums are a natural fit for the Cloudron. Decentralized "social"
networks are also good app candidates for the Cloudron.
## Image
Application images are created using [Docker](https://www.docker.io). Docker provides a way
to package (and containerize) the application as a filesystem which contains it's code, system libraries
and just about anything the app requires. This flexible approach allows the application to use just
about any language or framework.
Application images are instantiated as `containers`. Cloudron can run one or more isolated instances
of the same application as one or more containers.
Containerizing your application provides the following benefits:
* Apps run in the familiar environment that they were packaged for and can have libraries
and packages that are independent of the host OS.
* Containers isolate applications from one another.
The [base image](/references/baseimage.html) is the parent of all app images.
## Cloudron Manifest
Each app provides a `CloudronManifest.json` that specifies information required for the
`Cloudron Store` and for the installation of the image in the Cloudron.
Information required for container installation includes:
* List of `addons` like databases, caches, authentication mechanisms and file systems
* The http port on which the container is listening for incoming requests
* Additional TCP ports on which the application is listening to (for e.g., git, ssh,
irc protocols)
Information required for the Cloudron Store includes:
* Unique App Id
* Title
* Version
* Logo
See the [manifest reference](/references/manifest.html) for more information.
## Addons
Addons are services like database, authentication, email, caching that are part of the
Cloudron. Setup, provisioning, scaling and maintenance of addons is taken care of by the
Cloudron.
The fundamental idea behind addons is to allow resource sharing across applications.
For example, a single MySQL server instance can be used across multiple apps. The Cloudron
sets up addons in such a way that apps are isolated from each other.
Addons are opt-in and must be specified in the Cloudron Manifest. When the app runs, environment
variables contain the necessary information to access the addon. See the
[addon reference](/references/addons.html) for more information.
## Authentication
The Cloudron provides a centralized dashboard to manage users, roles and permissions. Applications
do not create or manage user credentials on their own and instead use one of the various
authentication strategies provided by the Cloudron.
Authentication strategies include OAuth 2.0, LDAP or Simple Auth. See the
[Authentication Reference](/references/authentication.html) for more information.
Authorizing users is application specific and it is only authentication that is delegated to the
Cloudron.
## Cloudron Store
Cloudron Store provides a market place to publish and optionally monetize your app. Submitting to the
Cloudron Store enables any Cloudron user to discover, purchase and install your application with
a few clicks.
## What next?
* [Package an existing app for the Cloudron](/tutorials/packaging.html)
+115
View File
@@ -0,0 +1,115 @@
# Authentication
## Overview
Cloudron provides a centralized dashboard to manage users, roles and permissions. Applications
do not create or manage user credentials on their own and instead use one of the various
authentication strategies provided by the Cloudron.
Note that authentication only identifies a user and does not indicate if the user is authorized
to perform an action in the application. Authorizing users is application specific and must be
implemented by the application.
## Users & Admins
Cloudron user management is intentionally very simple. The owner (first user) of the
Cloudron is `admin` by default. The `admin` role allows one to install, uninstall and reconfigure
applications on the Cloudron.
A Cloudron `admin` can create one or more users. Cloudron users can login and use any of the installed
apps in the Cloudron. In general, adding a cloudron user is akin to adding a person from one's family
or organization or team because such users gain access to all apps in the Cloudron. Removing a user
immediately revokes access from all apps.
A Cloudron `admin` can give admin privileges to one or more Cloudron users.
Each Cloudron user has an unique `username` and an `email`.
## Strategies
Cloudron provides multiple authentication strategies.
* OAuth 2.0 provided by the [OAuth addon](/references/addons.html#oauth)
* LDAP provided by the [LDAP addon](/references/addons.html#ldap)
* Simple Auth provided by [Simple Auth addon](/references/addons.html#simpleauth)
## Choosing a strategy
Applications can be broadly categorized based on their user management as follows:
* Multi-user aware
* Such apps have a full fledged user system and support multiple users and groups.
* These apps should use OAuth or LDAP.
* LDAP and OAuth APIs allow apps to detect if the user is a cloudron `admin`. Apps should use this flag
to show the application's admin panel for such users.
* No user
* Such apps have no concept of logged-in user.
* The Cloudron provides a `website visibility` setting that allows a Cloudron admin to optionally
install an OAuth proxy in front of such applications. In such a case, a user visiting the website first
authenticates with the OAuth proxy and once authenticated is allowed into the application.
* When an OAuth proxy is installed, such applications can use the `X-Authenticated-User` header from the
[ICAP Extensions](https://tools.ietf.org/html/draft-stecher-icap-subid-00#section-3.4) de facto standard.
This value can be used for display purposes or creating meta data for a document.
* Single user
* Such apps only have a single user who is usually also the `admin`.
* These apps can use Simple Auth or LDAP since they can authenticate users with a simple HTTP or LDAP request.
* Such apps _must_ set the `singleUser` property in the manifest which will restrict login to a single user
(configurable through the Cloudron's admin panel).
## Public and Private apps
`Private` apps display content only when they have a signed-in user. These apps can choose one of the
authentication strategies listed above.
`Public` apps display content to any visiting user (e.g a blog). These apps have a `login` url to allow
the editors & admins to login. This path can be optionally set as the `configurePath` in the manifest for
discoverability (for example, some blogs hide the login link).
Some apps allow the user to choose `private` or `public` mode or some other combination. Such configuration
is done at app install time and cannot be changed using a settings interface. It is tempting to show the user
a configuration dialog on first installation to switch the modes. This, however, leads the user to believe that
this configuration can be changed at any time later. In the case where this setting can be changed dynamically
from a settings ui in the app, it's better to simply put some sensible defaults and let the user discover
the settings. In the case where such settings cannot be changed dynamically, it is best to simply publish two
separate apps in the Cloudron store each with a different configuration.
## External User Registration
Some apps allow external users to register and create accounts. For example, a public company chat that
can invite anyone to join or a blog allowing registered commenters.
Such applications must track Cloudron users and external registered users independently (for example, using a flag).
As a thumb rule, apps must provide separate login buttons for each of the possible user sources. Such a design prevents
external users from (inadvertently) spoofing Cloudron users.
Naively handling user registration enables attacks of the following kind:
* An external user named `foo` registers in the app.
* A LDAP user named `foo` is later created on the Cloudron.
* When a user named `foo` logs in, the app cannot determine the correct `foo` anymore. Making separate login buttons for each
login source clears the confusion for both the user and the app.
## Userid
The preferred approach to track users in an application is a uuid or the Cloudron `username`.
The `username` in Cloudron is unique and cannot be changed.
Tracking users using `email` field is error prone since that may be changed by the user anytime.
## Single Sign-on
Single sign-on (SSO) is a property where a user logged in one application automatically logs into
another application without having to re-enter his credentials. When applications implement the
OAuth strategy, they automatically take part in Cloudron SSO. When a user signs in one application with
OAuth, they will automatically log into any other app implementing OAuth.
Conversely, signing off from one app, logs them off from all the apps.
## Security
The LDAP and Simple Auth strategies require the user to provide their plain text passwords to the
application. This might be a cause of concern and app developers are thus highly encouraged to integrate
with OAuth. OAuth also has the advantage of supporting Single Sign On.
+96
View File
@@ -0,0 +1,96 @@
# Base Image
## Overview
The application's Dockerfile must specify the FROM base image to be `cloudron/base:0.9.0`.
The base image already contains most popular software packages including node, nginx, apache,
ruby, PHP. Using the base image greatly reduces the size of app images.
The goal of the base image is simply to provide pre-downloaded software packages. The packages
are not configured in any way and it's up to the application to configure them as they choose.
For example, while `apache` is installed, there are no meaningful site configurations that the
application can use.
## Packages
The following packages are part of the base image. If you need another version, you will have to
install it yourself.
* Apache 2.4.18
* Composer 1.2.0
* Go 1.5.4, 1.6.3
* Gunicorn 19.4.5
* Java 1.8
* Maven 3.3.9
* Mongo 2.6.10
* MySQL Client 5.7.13
* nginx 1.10.0
* Node 0.10.40, 0.12.7, 4.2.6, 4.4.7 (installed under `/usr/local/node-<version>`) [more information](#node-js)
* Perl 5.22.1
* PHP 7.0.8
* Postgresql client 9.5.4
* Python 2.7.12
* Redis 3.0.6
* Ruby 2.3.1
* sqlite3 3.11.0
* Supervisor 3.2.0
* uwsgi 2.0.12
## Inspecting the base image
The base image can be inspected by installing [Docker](https://docs.docker.com/installation/).
Once installed, pull down the base image locally using the following command:
```
docker pull cloudron/base:0.9.0
```
To inspect the base image:
```
docker run -ti cloudron/base:0.9.0 /bin/bash
```
*Note:* Please use `docker 1.9.0` or above to pull the base image. Doing otherwise results in a base
image with an incorrect image id. The image id of `cloudron/base:0.9.0` is `d038af182821`.
## The `cloudron` user
The base image contains a user named `cloudron` that apps can use to run their app.
It is good security practice to run apps as a non-previleged user.
## Env vars
The following environment variables are set as part of the application runtime.
### API_ORIGIN
API_ORIGIN is set to the HTTP(S) origin of this Cloudron's API. For example,
`https://my-girish.cloudron.us`.
### APP_DOMAIN
APP_DOMAIN is set to the domain name of the application. For example, `app-girish.cloudron.us`.
### APP_ORIGIN
APP_ORIGIN is set to the HTTP(S) origin on the application. This is origin which the
user can use to reach the application. For example, `https://app-girish.cloudron.us`.
### CLOUDRON
CLOUDRON is always set to '1'. This is useful to write Cloudron specific code.
### WEBADMIN_ORIGIN
WEBADMIN_ORIGIN is set to the HTTP(S) origin of the Cloudron's web admin. For example,
`https://my-girish.cloudron.us`.
## Node.js
The base image comes pre-installed with various node.js versions.
They can be used by adding `ENV PATH /usr/local/node-<version>/bin:$PATH`.
See [Packages](/references/baseimage.html#packages) for available versions.
+93
View File
@@ -0,0 +1,93 @@
# Best practices
## Overview
This document explains the spirit of what makes a Cloudron app.
## No Setup
Cloudron apps do not show a setup screen after installation and should choose reasonable
defaults.
Databases, email configuration should be automatically picked up using [addons](/references/addons.html).
Admin role for the application can be detected dynamically using one of the [authentication](/references/authentication.html)
strategies.
## Image
The Dockerfile contains a specification for building an application image.
* Install any required software packages in the Dockerfile.
* Create static configuration files in the Dockerfile.
* Create symlinks to dynamic configuration files under `/run` in the Dockerfile.
* Docker supports restarting processes natively. Should your application crash, it will
be restarted automatically. If your application is a single process, you do not require
any process manager.
* The main process must handle `SIGTERM` and forward it as required to child processes. `bash`
does not automatically forward signals to child processes. For this reason, when using a startup
shell script, remember to use `exec <app>` as the last line. Doing so will replace bash with your
program and allows your program to handle signals as required.
* Use `supervisor`, `pm2` or any of the other process managers if you application has more
then one component. This excludes web servers like apache, nginx which can already manage their
children by themselves. Be sure to pick a process manager that forwards signals to child processes.
* Disable auto updates for apps. Updates must be triggered through the Cloudron Store. This allows the admin
to manage updates and downtime in a central location (the Cloudron Webadmin).
## File system
The Cloudron runs the application image as read-only. The app can only write to the following directories:
* `/tmp` - use this for temporary files.
* `/run` - use this for runtime configration and any dynamic data.
* `/app/data` - When the `localstorage` addon is enabled, any data under this directory is automatically backed up.
## Logging
Cloudron applications stream their logs to stdout and stderr. In contrast to logging
to files, this approach has many advantages:
* App does not need to rotate logs and the Cloudron takes care of managing logs
* App does not need special mechanism to release log file handles (on a log rotate)
* Integrates better with tooling like `cloudron cli`
This document gives you some recipes for configuring popular libraries to log to stdout. See
[base image](/references/baseimage.html#configuring) on how to configure various libraries to log to stdout/stderr.
## Memory
By default, applications get 256MB RAM (including swap). This can be changed using the `memoryLimit` field in the manifest.
Design your application runtime for concurrent use by 10s of users. The Cloudron is not designed for concurrent access by
100s or 1000s of users.
## Startup
* Apps must not present a post-installation screen on first run. It should be already pre-configured for
a specific purpose.
* Do not run as `root`. Apps can use the `cloudron` user which is part of the [base image](/references/baseimage.html)
for this purpose or create their own.
* When using the `localstorage` addon, the application must change the ownership of files in `/app/data` as desired using `chown`. This
is necessary because file permissions may not be correctly preserved across backup, restore, application and base image
updates.
* Addon information (mail, database) is exposed as environment variables. An application must use these values directly
and not cache them across restarts. If the variables are stored in a configuration file, then the configuration file
must be regenerated on every application start. This is usually done using a configuration template that is patched
on every startup.
## Authentication
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
This saves the user from having to manage separate set of users for different apps.
+47
View File
@@ -0,0 +1,47 @@
# Cloudron Button
The `Cloudron Button` allows anyone to install an application with
the click of a button on their Cloudron.
The button can be added to just about any website including the application's website
and README.md files in GitHub repositories.
## Prerequisites
The `Cloudron Button` is intended to work only for applications that have been
published on the Cloudron Store. The [basic tutorial](/tutorials/basic.html#publishing)
gives an overview of how to package and publish your application for the
Cloudron Store.
## HTML Snippet
```
<img src="https://cloudron.io/img/button32.png" href="https://cloudron.io/button.html?app=<appid>">
```
_Note_: Replace `<appid>` with your application's id.
## Markdown Snippet
```
[![Install](https://cloudron.io/img/button32.png)](https://cloudron.io/button.html?app=<appid>)
```
_Note_: Replace `<appid>` with your application's id.
## Button Height
The button may be used in different heights - 32, 48 and 64 pixels.
[![Install](/img/button32.png)](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
[![Install](/img/button48.png)](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
[![Install](/img/button64.png)](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
or as SVG
[![Install](/img/button.svg)](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
_Note_: Clicking the buttons above will install [Gogs](http://gogs.io/) on your Cloudron.
+470
View File
@@ -0,0 +1,470 @@
# CloudronManifest
## Overview
Every Cloudron Application contains a `CloudronManifest.json`.
The manifest contains two categories of information:
* Information about displaying the app on the Cloudron Store. For example,
the title, author information, description etc
* Information for installing the app on the Cloudron. This includes fields
like httpPort, tcpPorts.
A CloudronManifest.json can **only** contain fields that are listed as part of this
specification. The Cloudron Store and the Cloudron *may* reject applications that have
extra fields.
Here is an example manifest:
```
{
"id": "com.example.test",
"title": "Example Application",
"author": "Girish Ramakrishnan <girish@cloudron.io>",
"description": "This is an example app",
"tagline": "A great beginning",
"version": "0.0.1",
"healthCheckPath": "/",
"httpPort": 8000,
"addons": {
"localstorage": {}
},
"manifestVersion": 1,
"website": "https://www.example.com",
"contactEmail": "support@clourdon.io",
"icon": "file://icon.png",
"tags": [ "test", "collaboration" ],
"mediaLinks": [ "www.youtube.com/watch?v=dQw4w9WgXcQ" ]
}
```
## Fields
### addons
Type: object
Required: no
Allowed keys
* [ldap](addons.html#ldap)
* [localstorage](addons.html#localstorage)
* [mongodb](addons.html#mongodb)
* [mysql](addons.html#mysql)
* [oauth](addons.html#oauth)
* [postgresql](addons.html#postgresql)
* [redis](addons.html#redis)
* [sendmail](addons.html#sendmail)
The `addons` object lists all the [addons](addons.html) and the addon configuration used by the application.
Example:
```
"addons": {
"localstorage": {},
"mongodb": {}
}
```
### author
Type: string
Required: yes
The `author` field contains the name and email of the app developer (or company).
Example:
```
"author": "Cloudron Inc <girish@cloudron.io>"
```
### changelog
Type: markdown string
Required: no
The `changelog` field contains the changes in this version of the application. This string
can be a markdown style bulleted list.
Example:
```
"changelog": "* Add support for IE8 \n* New logo"
```
### configurePath
Type: path string
Required: no
The `configurePath` can be used to specify the absolute path to the configuration / settings
page of the app. When this path is present, an absoluted URL is constructed from the app's
install location this path and presented to the user in the configuration dialog of the app.
This is useful for apps that have a main page which does not display a configuration / settings
url (i.e) it's hidden for aesthetic reasons. For example, a blogging app like wordpress might
keep the admin page url hidden in the main page. Setting the configurationPath makes the
configuration url discoverable by the user.
Example:
```
"configurePath": "/wp-admin"
```
### contactEmail
Type: email
Required: yes
The `contactEmail` field contains the email address that Cloudron users can contact for any
bug reports and suggestions.
Example:
```
"contactEmail": "support@testapp.com"
```
### description
Type: markdown string
Required: yes
The `description` field contains a detailed description of the app. This information is shown
to the user when they install the app from the Cloudron Store.
Example:
```
"description": "This is a detailed description of this app."
```
A large `description` can be unweildy to manage and edit inside the CloudronManifest.json. For
this reason, the `description` can also contain a file reference. The Cloudron CLI tool fills up
the description from this file when publishing your application.
Example:
```
"description:": "file://DESCRIPTION.md"
```
### developmentMode
Type: boolean
Required: no
Setting `developmentMode` to true disables readonly rootfs and the default memory limit. In addition,
the application *pauses* on start and can be started manually using `cloudron exec`. Note that you
cannot submit an app to the store with this field turned on.
This mode can be used to identify the files being modified by your application - often required to
debug situations where your app does not run on a readonly rootfs. Run your app using `cloudron exec`
and use `find / -mmin -30` to find file that have been changed or created in the last 30 minutes.
### healthCheckPath
Type: url path
Required: yes
The `healthCheckPath` field is used by the Cloudron Runtime to determine if your app is running and
responsive. The app must return a 2xx HTTP status code as a response when this path is queried. In
most cases, the default "/" will suffice but there might be cases where periodically querying "/"
is an expensive operation. In addition, the app might want to use a specialized route should it
want to perform some specialized internal checks.
Example:
```
"healthCheckPath": "/"
```
### httpPort
Type: positive integer
Required: yes
The `httpPort` field contains the TCP port on which your app is listening for HTTP requests. This port
is exposed to the world via subdomain/location that the user chooses at installation time. While not
required, it is good practice to mark this port as `EXPOSE` in the Dockerfile.
Cloudron Apps are containerized and thus two applications can listen on the same port. In reality,
they are in different network namespaces and do not conflict with each other.
Note that this port has to be HTTP and not HTTPS or any other non-HTTP protocol. HTTPS proxying is
handled by the Cloudron platform (since it owns the certificates).
Example:
```
"httpPort": 8080
```
### icon
Type: local image filename
Required: no
The `icon` field is used to display the application icon/logo in the Cloudron Store. Icons are expected
to be square of size 256x256.
```
"icon": "file://icon.png"
```
### id
Type: reverse domain string
Required: yes
The `id` is a unique human friendly Cloudron Store id. This is similar to reverse domain string names used
as java package names. The convention is to base the `id` based on a domain that you own.
The Cloudron tooling allows you to build applications with any `id`. However, you will be unable to publish
the application if the id is already in use by another application.
```
"id": "io.cloudron.testapp"
```
### manifestVersion
Type: integer
Required: yes
`manifestVersion` specifies the version of the manifest and is always set to 1.
```
"manifestVersion": 1
```
### mediaLinks
Type: array of urls
Required: no
The `mediaLinks` field contains an array of links that the Cloudron Store uses to display a slide show of pictures
and videos of the application.
All links are preferably https.
```
"mediaLinks": [
"www.youtube.com/watch?v=dQw4w9WgXcQ",
"https://images.rapgenius.com/fd0175ef780e2feefb30055be9f2e022.520x343x1.jpg"
]
```
### memoryLimit
Type: bytes (integer)
Required: no
The `memoryLimit` field is the maximum amount of memory (including swap) in bytes an app is allowed to consume before it
gets killed and restarted.
By default, all apps have a memoryLimit of 256MB. For example, to have a limit of 500MB,
```
"memoryLimit": 524288000
```
### maxBoxVersion
Type: semver string
Required: no
The `maxBoxVersion` field is the maximum box version that the app can possibly run on. Attempting to install the app on
a box greater than `maxBoxVersion` will fail.
This is useful when a new box release introduces features which are incompatible with the app. This situation is quite
unlikely and it is recommended to leave this unset.
### minBoxVersion
Type: semver string
Required: no
The `minBoxVersion` field is the minimum box version that the app can possibly run on. Attempting to install the app on
a box lesser than `minBoxVersion` will fail.
This is useful when the app relies on features that are only available from a certain version of the box. If unset, the
default value is `0.0.1`.
### postInstallMessage
Type: markdown string
Required: no
The `postInstallMessageField` is a message that is displayed to the user after an app is installed.
The intended use of this field is to display some post installation steps that the user has to carry out to
complete the installation. For example, displaying the default admin credentials and informing the user to
to change it.
### singleUser
Type: boolean
Required: no
The `singleUser` field can be set to true for apps that are meant to be used only a single user.
When set, the Cloudron will display a user selection dialog at installation time. The selected user is the sole user
who can access the app.
### tagline
Type: one-line string
Required: no
The `tagline` is used by the Cloudron Store to display a single line short description of the application.
```
"tagline": "The very best note keeper"
```
### tags
Type: Array of strings
Required: no
The `tags` are used by the Cloudron Store for filtering searches by keyword.
```
"tags": [ "git", "version control", "scm" ]
```
### targetBoxVersion
Type: semver string
Required: no
The `targetBoxVersion` field is the box version that the app was tested on. By definition, this version has to be greater
than the `minBoxVersion`.
The box uses this value to enable compatibility behavior of APIs. For example, an app sets the targetBoxVersion to 0.0.5
and is published on the store. Later, box version 0.0.10 introduces a new feature that conflicts with how apps used
to run in 0.0.5 (say SELinux was enabled for apps). When the box runs such an app, it ensures compatible behavior
and will disable the SELinux feature for the app.
If unspecified, this value defaults to `minBoxVersion`.
### tcpPorts
Type: object
Required: no
Syntax: Each key is the environment variable. Each value is an object containing `title`, `description` and `defaultValue`.
An optional `containerPort` may be specified.
The `tcpPorts` field provides information on the non-http TCP ports/services that your application is listening on. During
installation, the user can decide how these ports are exposed from their Cloudron.
For example, if the application runs an SSH server at port 29418, this information is listed here. At installation time,
the user can decide any of the following:
* Expose the port with the suggested `defaultValue` to the outside world. This will only work if no other app is being exposed at same port.
* Provide an alternate value on which the port is to be exposed to outside world.
* Disable the port/service.
To illustrate, the application lists the ports as below:
```
"tcpPorts": {
"SSH_PORT": {
"title": "SSH Port",
"description": "SSH Port over which repos can be pushed & pulled",
"defaultValue": 29418,
"containerPort": 22
}
},
```
In the above example:
* `SSH_PORT` is an app specific environment variable. Only strings, numbers and _ (underscore) are allowed. The author has to ensure that they don't clash with platform profided variable names.
* `title` is a short one line information about this port/service.
* `description` is a multi line description about this port/service.
* `defaultValue` is the recommended port value to be shown in the app installation UI.
* `containerPort` is the port that the app is listening on (recall that each app has it's own networking namespace).
In more detail:
* If the user decides to disable the SSH service, this environment variable `SSH_PORT` is absent. Applications _must_ detect this on
start up and disable these services.
* `SSH_PORT` is set to the value of the exposed port. Should the user choose to expose the SSH server on port 6000, then the
value of SSH_PORT is 6000.
* `defaultValue` is **only** used for display purposes in the app installation UI. This value is independent of the value
that the app is listening on. For example, the app can run an SSH server at port 22 but still recommend a value of 29418 to the user.
* `containerPort` is the port that the app is listening on. The Cloudron runtime will _bridge_ the user chosen external port
with the app specific `containerPort`. Cloudron Apps are containerized and each app has it's own networking namespace.
As a result, different apps can have the same `containerPort` value because these values are namespaced.
* The environment variable `SSH_PORT` may be used by the app to display external URLs. For example, the app might want to display
the SSH URL. In such a case, it would be incorrect to use the `containerPort` 22 or the `defaultValue` 29418 since this is not
the value chosen by the user.
* `containerPort` is optional and can be omitted, in which case the bridged port numbers are the same internally and externally.
Some apps use the same variable (in their code) for listen port and user visible display strings. When packaging these apps,
it might be simpler to listen on `SSH_PORT` internally. In such cases, the app can omit the `containerPort` value and should
instead reconfigure itself to listen internally on `SSH_PORT` on each start up.
### title
Type: string
Required: yes
The `title` is the primary application title displayed on the Cloudron Store.
Example:
```
"title": "Gitlab"
```
### version
Type: semver string
Required: yes
The `version` field specifies a [semver](http://semver.org/) string. The version is used by the Cloudron to compare versions and to
determine if an update is available.
Example:
```
"version": "1.1.0"
```
### website
Type: url
Required: yes
The `website` field is a URL where the user can read more about the application.
Example:
```
"website": "https://example.com/myapp"
```
+61
View File
@@ -0,0 +1,61 @@
# Configuration Recipes
## nginx
`nginx` is often used as a reverse proxy in front of the application, to dispatch to different backend programs based on the request route or other characteristics. In such a case it is recommended to run nginx and the application through a process manager like `supervisor`.
Example nginx supervisor configuration file:
```
[program:nginx]
directory=/tmp
command=/usr/sbin/nginx -g "daemon off;"
user=root
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
```
The nginx configuration, provided with the base image, can be used by adding an application specific config file under `/etc/nginx/sites-enabled/` when building the docker image.
```
ADD <app config file> /etc/nginx/sites-enabled/<app config file>
```
Since the base image nginx configuration is unpatched from the ubuntu package, the application configuration has to ensure nginx is using `/run/` instead of `/var/lib/nginx/` to support the read-only filesystem nature of a Cloudron application.
Example nginx app config file:
```
client_body_temp_path /run/client_body;
proxy_temp_path /run/proxy_temp;
fastcgi_temp_path /run/fastcgi_temp;
scgi_temp_path /run/scgi_temp;
uwsgi_temp_path /run/uwsgi_temp;
server {
listen 8000;
root /app/code/dist;
location /api/v1/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
```
## supervisor
Use this in the program's config:
```
[program:app]
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```
+292
View File
@@ -0,0 +1,292 @@
# Self host Cloudron
The Cloudron platform can be installed on your own cloud server. The self hosted version comes with all the same features as the managed version.
## CLI Tool
The [Cloudron tool](https://git.cloudron.io/cloudron/cloudron-cli) is used for managing a Cloudron. It has a `machine`
subcommand that can be used to create, update and maintain a self-hosted Cloudron.
### Linux & OS X
Installing the CLI tool requires node.js and npm. The CLI tool can be installed using the following command:
```
npm install -g cloudron
```
Depending on your setup, you may need to run this as root.
On OS X, it is known to work with the `openssl` package from homebrew.
See [#14](https://git.cloudron.io/cloudron/cloudron-cli/issues/14) for more information.
### Windows
The CLI tool does not work on Windows.
### Machine subcommands
You should now be able to run the `cloudron machine help` command in a shell.
```
create Creates a new Cloudron
restore Restores a Cloudron
migrate Migrates a Cloudron
update Upgrade or updates a Cloudron
eventlog Get Cloudron eventlog
logs Get Cloudron logs
ssh Get remote SSH connection
backup Manage Cloudron backups
```
## AWS EC2
### Requirements
To run the Cloudron on AWS, first sign up with [Amazon AWS](https://aws.amazon.com/).
The Cloudron uses the following AWS services:
* **EC2** for creating a virtual private server that runs the Cloudron code.
* **Route53** for DNS. The Cloudron will manage all app subdomains as well as the email related DNS records automatically.
* **S3** to store encrypted Cloudron backups.
The minimum requirements for a Cloudron depends on the apps installed. The absolute minimum required EC2 instance is `t2.small`.
The Cloudron runs best on instances which do not have a burst mode VCPU.
The system disk space usage of a Cloudron is around 15GB. This results in a minimum requirement of about 30GB to give some headroom for app installations and user data.
### Cost Estimation
Taking the minimal requirements of hosting on EC2, with a backup retention of 2 days, the cost estimation per month is as follows:
```
Route53: 0.90
EC2: 19.04
EBS: 3.00
S3: 1.81
-------------------------
Total: $ 24.75/mth
```
For custom cost estimation, please use the [AWS Cost Calculator](http://calculator.s3.amazonaws.com/index.html)
### Setup
Open the AWS console and create the required resources:
1. Create a Route53 zone for your domain. Be sure to set the Route53 nameservers for your domain in your name registrar. Note: Only Second Level Domains are supported.
For example, `example.com`, `example.co.uk` will work fine. Choosing a domain name at any other level like `cloudron.example.com` will not work.
2. Create a S3 bucket for backups. The bucket region **must* be the same region as where you intend to create your Cloudron (EC2).
When creating the S3 bucket, it is important to choose a region. Do **NOT** choose `US Standard`.
The supported regions are:
* US East (N. Virginia) us-east-1
* US West (N. California) us-west-1
* US West (Oregon) us-west-2
* Asia Pacific (Mumbai) ap-south-1
* Asia Pacific (Seoul) ap-northeast-2
* Asia Pacific (Sydney) ap-southeast-2
* Asia Pacific (Tokyo) ap-northeast-1
* EU (Frankfurt) eu-central-1
* EU (Ireland) eu-west-1
* South America (São Paulo) sa-east-1
3. Create a new SSH key or upload an existing SSH key in the target region (`Key Pairs` in the left pane of the EC2 console).
4. Create AWS credentials. You can either use root **or** IAM credentials.
* For root credentials:
* In AWS Console, under your name in the menu bar, click `Security Credentials`
* Click on `Access Keys` and create a key pair.
* For IAM credentials:
* You can use the following policy to create IAM credentials:
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "route53:*",
"Resource": [
"arn:aws:route53:::hostedzone/<hosted zone id>"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:GetChange"
],
"Resource": [
"*"
]
},
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::<your bucket name>",
"arn:aws:s3:::<your bucket name>/*"
]
},
{
"Effect": "Allow",
"Action": "ec2:*",
"Resource": [
"*"
],
"Condition": {
"StringEquals": {
"ec2:Region": "<ec2 region>"
}
}
}
]
}
```
### Create the Cloudron
Create the Cloudron using the `cloudron machine` command:
```
cloudron machine create ec2 \
--region <aws-region> \
--type t2.small \
--disk-size 30 \
--ssh-key <ssh-key-name-or-filepath> \
--access-key-id <aws-access-key-id> \
--secret-access-key <aws-access-key-secret> \
--backup-bucket <bucket-name> \
--backup-key '<secret>' \
--fqdn <domain>
```
The `--region` is the region where your Cloudron is to be created. For example, `us-west-1` for N. California and `eu-central-1` for Frankfurt. A complete list of available
regions is list <a href="//docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions" target="_blank">here</a>.
The `--disk-size` parameter indicates the volume (hard disk) size to be allocated for the Cloudron.
The `--ssh-key` is the path to a PEM file or the private SSH Key. If your key is located as `~/.ssh/id_rsa_<name>`, you can
also simply provide the `name` as the argument.
The `--backup-key '<secret>'` will be used to encrypt all backups prior to uploading to S3. Keep that secret in a safe place, as you need it to restore your Cloudron from a backup! You can generate a random key using `pwgen -1y 64`. Be sure to put single quotes
around the `secret` to prevent accidental shell expansion.
**NOTE**: The `cloudron machine create` subcommand will automatically create a corresponding VPC, subnet and security group for your Cloudron, unless `--subnet` and `--security-group` arguments are explicitly passed in. If you want to reuse existing resources, please ensure that the security group does not limit any traffic to the Cloudron since the Cloudron manages its own firewall and that the subnet has an internet gateway setup in the routing table.
## First time setup
Visit `https://my.<domain>` to do first time setup of your Cloudron.
1. The website should already have a valid TLS certificate. If you see any certificate warnings, it means your Cloudron was not created correctly.
2. If you see a login screen, instead of a setup screen, it means that someone else got to your Cloudron first and set it up
already! In this unlikely case, simply delete the EC2 instance and create a new Cloudron again.
Once the setup is done, you can access the admin page in the future at `https://my.<domain>`.
## Backups
The Cloudron has a backup schedule of creating one once a day. In addition to regularly scheduled backups, a backup is also created if you update the Cloudron or any of the apps (in this case only the app in question will get backed up).
Since this might result in a lot of backup data on your S3 backup bucket, we recommend adjusting the bucket properties. This can be done adding a lifecycle rule for that bucket, using the AWS console. S3 supports both permanent deletion or moving objects to the cheaper Glacier storage class based on an age attribute. With the current daily backup schedule a setting of two days should be already sufficient for most use-cases.
If your Cloudron is running, you can list backups using the following command:
```
cloudron machine backup list <domain>
```
Alternately, you can list the backups by querying S3 using the following command:
```
cloudron machine backup list --provider ec2 \
--region <region> \
--access-key-id <access-key-id> \
--secret-access-key <secret-access-key> \
--backup-bucket <s3 bucket name> \
<domain>
```
## Restore
The Cloudron can restore itself from a backup using the following command:
```
cloudron machine create ec2 \
--backup <backup-id> \
--region <aws-region> \
--type t2.small \
--disk-size 30 \
--ssh-key <ssh-key-name> \
--access-key-id <aws-access-key-id> \
--secret-access-key <aws-access-key-secret> \
--backup-bucket <bucket-name> \
--backup-key <secret> \
--fqdn <domain>
```
The backup id can be obtained by [listing the backup](/references/selfhosting.html#backups). Other arguments are similar to [Cloudron creation](/references/selfhosting.html#create-the-cloudron). Once the new instance has completely restored, you can safely terminate the old Cloudron from the AWS console.
## Updates
Apps installed from the Cloudron Store are updated automatically every night.
The Cloudron platform itself updates in two ways:
* An **update** is applied onto the running server instance. Such updates are performed every night. You can use the Cloudron UI to perform updates.
* An **upgrade** requires a new OS image and thus has to be performed using the CLI tool. This process involves creating a new EC2 instance is created using the latest image and all the data and apps are restored. The `cloudron machine update` command can be used when an _upgrade_ is available (you will get a notification in the UI).
```
cloudron machine update --ssh-key <ssh-key> <domain>
```
Once the upgrade is complete, you can safely terminate the old EC2 instance.
The Cloudron will always make a complete backup before attempting an update or upgrade. In the unlikely case an update fails, it can be [restored](/references/selfhosting.html#restore).
## SSH
If you want to SSH into your Cloudron, you can
```
ssh -p 202 -i ~/.ssh/ssh_key_name root@my.<domain>
```
If you are unable to connect, verify the following:
* Be sure to use the **my.** subdomain (eg. my.foobar.com).
* The SSH Key should be in PEM format. If you are using Putty PPK files, follow [this article](http://stackoverflow.com/questions/2224066/how-to-convert-ssh-keypairs-generated-using-puttygenwindows-into-key-pairs-use) to convert it to PEM format.
* The SSH Key must have correct permissions (400) set (this is a requirement of the ssh client).
## Mail
Spammers frequently abuse EC2 public IP addresses and as a result your Cloudron might possibly start out with a bad
reputation. The good news is that most IP based blacklisting services cool down over time. The Cloudron
sets up DNS entries for SPF, DKIM automatically and reputation should be easy to get back.
* Once your Cloudron is ready, apply for a Reverse DNS record to be setup for your domain. You can find the AWS request
form [here](https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/ec2-email-limit-rdns-request).
* Check if your IP is listed in any DNSBL list [here](http://multirbl.valli.org/). In most cases, you can apply for removal
of your IP by filling out a form at the DNSBL manager site.
* Finally, check your spam score at [mail-tester.com](https://www.mail-tester.com/).
## Debugging
To debug the Cloudron CLI tool:
* `DEBUG=* cloudron <cmd>`
You can also [SSH](#ssh) into your Cloudron and collect logs.
* `journalctl -a -u box -u cloudron-installer` to get debug output of box related code.
* `docker ps` will give you the list of containers. The addon containers are named as `mail`, `postgresql`, `mysql` etc. If you want to get a specific
containers log output, `journalctl -a CONTAINER_ID=<container_id>`.
## Other Providers
Currently, we do not support other cloud server provider. Please let us know at [support@cloudron.io](mailto:support@cloudron.io), if you want to see other providers supported.
## Help
If you run into any problems, join us in our [chat](https://chat.cloudron.io) or [email us](mailto:support@cloudron.io).
+329
View File
@@ -0,0 +1,329 @@
# User Manual
## Introduction
The Cloudron is the best platform self-hosting web applications on your server. You
can easily install apps on it, add users, manage access restriction and keep your
server and apps updated with no effort.
You might wonder that there are so many 1-click app solutions out there and what is so special
about Cloudron? As the name implies, 1-click installers simply install code into a server
and leave it at that. There's so much more to do:
1. Configure a domain to point to your server
2. Setup SSL certificates and renew them periodically
3. Ensure apps are backed up correctly
4. Ensure apps are uptodate and secure
5. Have a mechanism to quickly restore apps from a backup
6. Manage users across all your apps
7. Get alerts and notifications about the status of apps
... and so on ...
We made the Cloudron to dramatically lower the bar for people to run apps on servers. Just provide
a domain name, install apps and add users. All the server management tasks listed above is
completely automated.
If you want to learn more about the secret sauce that makes the Cloudron, please read our
[architecture overview](/references/architecture.html).
## Use cases
Here are some of the apps you can run on a Cloudron:
* RSS Reader
* Chat, IRC, Jabber servers
* Public forum
* Blog
* File syncing and sharing
* Code hosting
* Email
Our list of apps is growing everyday, so be sure to [follow us on twitter](https://twitter.com/cloudron_io).
## Activation
When you first create the Cloudron, the setup wizard will ask you to setup an administrator
account. Don't worry, a Cloudron adminstrator doesn't need to know anything about maintaining
a server! It's the whole reason why we made the Cloudron. Being a Cloudron administrator is
more analagous to being the owner of a smartphone. You can always add more administrators to
the Cloudron from the `Users` menu item.
<img src="/docs/img/webadmin_domain.png" class="shadow">
The Cloudron administration page is located at the `my` subdomain. You might want to bookmark
this link!
## Apps
### Installation
You can install apps on the Cloudron by choosing the `App Store` menu item. Use the 'Search' bar
to search for apps.
Clicking on app gives you information about the app.
<img src="/docs/img/app_info.png" class="shadow">
Clicking the `Install` button will show an install dialog like below:
<img src="/docs/img/app_install.png" class="shadow">
The `Location` field is the subdomain in which your app will be installed. For example, if you use the
`mail` location for your web mail client, then it will be accessible at `mail.<domain>`.
Tip: You can access the apps directly on your browser using `mail.<domain>`. You don't have to
visit the Cloudron administration panel.
`Access control` specifies who can access this app.
* `Every Cloudron user` - Any user in your Cloudron can access the app. Initially, you are the only
user in your Cloudron. Unless you explicitly invite others, nobody else can access these apps.
Note that the term 'access' depends on the app. For a blog, this means that nobody can post new
blog posts (but anybody can view them). For a chat server, this might mean that nobody can access
your chat server.
* `Restrict to groups` - Only users in the groups can access the app.
### Updates
All your apps automatically update as and when the application author releases an update. The Cloudron
will attempt to update around midnight of your timezone.
Some app updates are not automatic. This can happen if a new version of the app has removed some features
that you were relying on. In such a case, the update has to be manually approved. This is simply a matter
of clicking the `Update` button (the green star) after you read about the changes.
<img src="/docs/img/app_update.png" class="shadow">
### Backups
All apps are automatically backed up every day. Backups are stored encrypted in Amazon S3. You don't have
to do anything about it. The [Cloudron CLI](https://git.cloudron.io/cloudron/cloudron-cli) tool can be used
to download application backups.
### Configuration
Apps can be reconfigured using the `Configure` button.
<img src="/docs/img/app_configure_button.png" class="shadow">
Click on the wrench button will bring up the configure dialog.
<img src="/docs/img/app_configure.png" class="shadow">
You can do the following:
* Change the location to move the app to another subdomain. Say, you want to move your blog from `blog` to `about`.
* Change who can access the app.
Changing an app's configuration has a small downtime (usually around a minute).
### Restore
Apps can be restored to a previous backup by clicking on the `Restore` button.
<img src="/docs/img/app_restore_button.png" class="shadow">
Note that restoring previous data might also restore the previous version of the software. For example, you might
be currently using Version 5 of the app. If you restore to a backup that was made with Version 3 of the app, then the restore
operation will install Version 3 of the app. This is because the latest version may not be able to handle old data.
### Uninstall
You can uninstall an app by clicking the `Uninstall` button.
<img src="/docs/img/app_uninstall_button.png" class="shadow">
Note that all data associated with the app will be immediately removed from the Cloudron. App data might still
persist in your old backups and the [CLI tool](https://git.cloudron.io/cloudron/cloudron-cli) provides a way to
restore from those old backups should it be required.
### Embedding Apps
It is possible to embed Cloudron apps into other websites. By default, this is disabled to prevent
[Clickjacking](https://cloudron.io/blog/2016-07-15-site-embedding.html).
You can set a website that is allowed to embed your Cloudron app using the app's [Configure dialog](#configuration).
Click on 'Show Advanced Settings...' and enter the embedder website name.
## Custom domain
When you create a Cloudron from cloudron.io, we provide a subdomain under `cloudron.me` like `girish.cloudron.me`.
Apps are available under that subdomain using a hyphenated name like `blog-girish.cloudron.me`.
Domain names are a thing of pride and the Cloudron makes it easy to make your apps accessible from memorable locations like `blog.girish.in`.
### Single app on a custom domain
This approach is applicable if you desire that only a single app be accessing from a custom
domain. For this, open the app's configure dialog and choose `External Domain` in the location dropdown.
<img src="/docs/img/app_external_domain.png" class="shadow">
This dialog will suggest you to add a `CNAME` record. Once you setup a CNAME record with your DNS provider,
the app will be accessible from that external domain.
### Entire Cloudron on a custom domain
This approach is applicable if you want all your apps to be accessible from subdomains of your custom domain.
For example, `blog.girish.in`, `notes.girish.in`, `owncloud.girish.in`, `mail.girish.in` and so on. This
approach is also the only way that the Cloudron supports for sending and receiving emails from your domain.
For this, go to the 'Domains & Certs' menu item.
<img src="/docs/img/custom_domain_menu.png" class="shadow">
Change the domain name to your custom domain. Currently, we require that your domain be hosted on AWS Route53.
<img src="/docs/img/custom_domain_change.png" class="shadow">
Moving to a custom domain will retain all your apps and data and will take around 15 minutes. If you require assistance with another provider,
<a href="mailto:support@cloudron.io">just let us know</a>.
## User management
### Users
You can invite new users (friends, family, colleagues) with their email address from the `Users` menu. They will
receive an invite to sign up with your Cloudron. They can now access the apps that you have given them access
to.
<img src="/docs/img/users.png" class="shadow">
To remove a user, simply remove them from the list. Note that the removed user cannot access any app anymore.
### Administrators
A Cloudron administrator is a special right given to an existing Cloudron user allowing them to manage
apps and users. To make an existing user an administator, click the edit (pencil) button corresponding to
the user and check the `Allow this user to manage apps, groups and other users` checkbox.
<img src="/docs/img/administrator.png" class="shadow">
### Groups
Groups provide a convenient way to restrict access to your apps. Simply add one or more users to a group
and restrict the access for an app to that group. You can create a group by using the `Groups` menu item.
<img src="/docs/img/groups.png" class="shadow">
To set the access restriction use the app's configure dialog.
<img src="/docs/img/app_access_control.png" class="shadow">
## Login
### Cloudron admin
The Cloudron admin page is always located at the `my` subdomain of your Cloudron domain. For custom domains,
this will be like `my.girish.in`. For domains from cloudron.io, this will be like `my-girish.cloudron.me`.
### Apps (single sign-on)
An important feature of the Cloudron is Single Sign-On. You use the same username & password for logging in
to all your apps. No more having to manage separate set of credentials for each service!
### Single user apps
Some apps only work with a single user. For example, a notes app might allow only a single user to login and add
notes. For such apps, you will be prompted during installation to select the single user who can access the app.
<img src="/docs/img/app_single_user.png" class="shadow">
If you want multiple users to use the app independently, simply install the app multiple times to different locations.
## Email
The Cloudron has a built-in email server. The primary email address is the same as the username. Emails can be sent
and received from `<username>@<domain>`. The Cloudron does not allow masquerading - one user cannot send email
pretending to be another user.
### Receiving email (IMAP)
Use the following settings to receive email.
* Server Name - Use the `my` subdomain of your Cloudron
* Port - 993
* Connection Security - TLS
* Username/password - Same as your Cloudron credentials
### Sending email (SMTP)
Use the following settings to send email.
* Server Name - Use the `my` subdomain of your Cloudron
* Port - 587
* Connection Security - STARTTLS
* Username/password - Same as your Cloudron credentials
### Email filters (Sieve)
Use the following settings to setup email filtering users via Manage Sieve.
* Server Name - Use the `my` subdomain of your Cloudron
* Port - 4190
* Connection Security - TLS
* Username/password - Same as your Cloudron credentials
The [Rainloop](https://cloudron.io/appstore.html?app=net.rainloop.cloudronapp) and [Roundcube](https://cloudron.io/appstore.html?app=net.roundcube.cloudronapp)
apps are already pre-configured to use the above settings.
### Aliases
You can configure one or more aliases alongside the primary email address of each user. You can set aliases by editing the
user's settings, available behind the edit button in the user listing. Note that aliases cannot conflict with existing user names.
<img src="/docs/img/email_alias.png" class="shadow">
Currently, it is not possible to login using the alias for SMTP/IMAP/Sieve services. Instead, add the alias as an identity in
your mail client but login using the Cloudron credentials.
### Subaddresses
Emails addressed to `<username>+tag@<domain>` will be delivered to the `username` mailbox. You can use this feature to give out emails of the form
`username+kayak@<domain>`, `username+aws@<domain>` and so on and have them all delivered to your mailbox.
## Graphs
The Graphs view shows an overview of the disk and memory usage on your Cloudron.
<img src="/docs/img/graphs.png" class="shadow">
The `Disk Usage` graph shows you how much disk space you have left. Note that the Cloudron will
send the Cloudron admins an email notification when the disk is ~90% full.
The `Apps` Memory graph shows the memory consumed by each installed app. You can click on each segment
on the graph to see the memory consumption over time in the chart below it.
The `System` Memory graph shows the overall memory consumption on the entire Cloudron. If you see
the Free memory < 50MB frequently, you should consider upgrading to a Cloudron with more memory.
## Activity log
The `Activity` view shows the activity on your Cloudron. It includes information about who is using
the apps on your Cloudron and also tracks configuration changes.
<img src="/docs/img/activity.png" class="shadow">
## Domains and SSL Certificates
All apps on the Cloudron can only be reached by `https`. The Cloudron automatically installs and
renews certificates for your apps as needed. Should installation of certificate fail for reasons
beyond it's control, Cloudron admins will get a notification about it.
## API Access
All the operations listed in this manual like installing app, configuring users and groups, are
completely programmable with a [REST API](/references/api.html).
## Moving to a larger Cloudron
When using a Cloudron from cloudron.io, it is easy to migrate your apps and data to a bigger server.
In the `Settings` page, you can change the plan.
<insert picture>
## Command line tool
If you are a software developer or a sysadmin, the Cloudron comes with a CLI tool that can be
used to develop custom apps for the Cloudron. Read more about it [here](https://git.cloudron.io/cloudron/cloudron-cli).
+621
View File
@@ -0,0 +1,621 @@
# Overview
This tutorial provides an introduction to developing applications
for the Cloudron using node.js.
# Installation
## Install CLI tool
The Cloudron CLI tool allows you to install, configure and test apps on your Cloudron.
Installing the CLI tool requires [node.js](https://nodejs.org/) and
[npm](https://www.npmjs.com/). You can then install the CLI tool using the following
command:
```
sudo npm install -g cloudron
```
Note: Depending on your setup, you can run the above command without `sudo`.
## Testing your installation
The `cloudron` command should now be available in your path.
Let's login to the Cloudron as follows:
```
$ cloudron login
Cloudron Hostname: craft.selfhost.io
Enter credentials for craft.selfhost.io:
Username: girish
Password:
Login successful.
```
## Your First Application
Creating an application for Cloudron can be summarized as follows:
1. Create a web application using any language/framework. This web application must run a HTTP server
and can optionally provide other services using custom protocols (like git, ssh, TCP etc).
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
an application ```image```. An ```image``` is essentially a bundle of the application source code
and it's dependencies.
3. Create a [CloudronManifest.json](/references/manifest.html) file that provides essential information
about the app. This includes information required for the Cloudron Store like title, version, icon and
runtime requirements like `addons`.
## Simple Web application
To keep things simple, we will start by deploying a trivial node.js server running on port 8000.
Create a new project folder `tutorial/` and add a file named `tutorial/server.js` with the following content:
```javascript
var http = require("http");
var server = http.createServer(function (request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello World\n");
});
server.listen(8000);
console.log("Server running at port 8000");
```
## Dockerfile
A Dockerfile contains commands to assemble an image.
Create a file named `tutorial/Dockerfile` with the following content:
```dockerfile
FROM cloudron/base:0.9.0
ADD server.js /app/code/server.js
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
```
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
All Cloudron apps **must** start from this base image.
The `ADD` command copies the source code of the app into the directory `/app/code`.
While this example only copies a single file, the ADD command can be used to copy directory trees as well.
See the [Dockerfile](https://docs.docker.com/reference/builder/#add) documentation for more details.
The `CMD` command specifies how to run the server. There are multiple versions of node available under `/usr/local`. We
choose node v0.12.7 for our app.
## CloudronManifest.json
The `CloudronManifest.json` specifies
* Information about displaying the app on the Cloudron Store. For example,
the title, author information, description etc
* Information for installing the app on the Cloudron. This includes fields
like httpPort, tcpPorts.
Create the CloudronManifest.json using the following command:
```
$ cloudron init
id: io.cloudron.tutorial # unique id for this app. use reverse domain name convention
author: John Doe # developer or company name of the for user <email>
title: Tutorial App # Cloudron Store title of this app
description: App that uses node.js # A string or local file reference like file://DESCRIPTION.md
tagline: Changing the world one app at a time # A tag line for this app for the Cloudron Store
website: https://cloudron.io # A link to this app's website
contactEmail: support@cloudron.io # Contact email of developer or company
httPort: 8000 # The http port on which this application listens to
```
The above command creates a CloudronManifest.json:
File ```tutorial/CloudronManifest.json```
```json
{
"id": "io.cloudron.tutorial",
"author": "John Doe",
"title": "Tutorial App",
"description": "App that uses node.js",
"tagline": "Changing the world one app at a time",
"version": "0.0.1",
"healthCheckPath": "/",
"httpPort": 8000,
"addons": {
"localstorage": {}
},
"minBoxVersion": "0.0.1",
"manifestVersion": 1,
"website": "https://cloudron.io",
"contactEmail": "support@cloudron.io",
"icon": "",
"mediaLinks": []
}
```
You can read in more detail about each field in the [Manifest reference](/references/manifest.html).
# Installing
## Building
We now have all the necessary files in place to build and deploy the app to the Cloudron.
Building creates an image of the app using the Dockerfile which can then be used to deploy
to the Cloudron.
Building, pushing and pulling docker images is very bandwidth and CPU intensive. To alleviate this
problem, apps are built using the `build service` which uses `cloudron.io` account credentials.
**Warning**: As of this writing, the build service uses the public Docker registry and the images that are built
can be downloaded by anyone. This means that your source code will be viewable by others.
Initiate a build using ```cloudron build```:
```
$ cloudron build
Building io.cloudron.tutorial@0.0.1
Appstore login:
Email: ramakrishnan.girish@gmail.com # cloudron.io account
Password: # Enter password
Login successful.
Build scheduled with id 76cebfdd-7822-4f3d-af17-b3eb393ae604
Downloading source
Building
Step 0 : FROM cloudron/base:0.9.0
---> 97583855cc0c
Step 1 : ADD server.js /app/code
---> b09b97ecdfbc
Removing intermediate container 03c1e1f77acb
Step 2 : CMD /usr/local/node-0.12.7/bin/node /app/code/main.js
---> Running in 370f59d87ab2
---> 53b51eabcb89
Removing intermediate container 370f59d87ab2
Successfully built 53b51eabcb89
The push refers to a repository [cloudron/img-2074d69134a7e0da3d6cdf3c53e241c4] (len: 1)
Sending image list
Pushing repository cloudron/img-2074d69134a7e0da3d6cdf3c53e241c4 (1 tags)
Image already pushed, skipping 57f52d167bbb
Image successfully pushed b09b97ecdfbc
Image successfully pushed 53b51eabcb89
Pushing tag for rev [53b51eabcb89] on {https://cdn-registry-1.docker.io/v1/repositories/cloudron/img-2074d69134a7e0da3d6cdf3c53e241c4/tags/76cebfdd-7822-4f3d-af17-b3eb393ae604}
Build succeeded
```
## Installing
Now that we have built the image, we can install our latest build on the Cloudron
using the following command:
```
$ cloudron install
Using cloudron craft.selfhost.io
Using build 76cebfdd-7822-4f3d-af17-b3eb393ae604 from 1 hour ago
Location: tutorial # This is the location into which the application installs
App is being installed with id: 4dedd3bb-4bae-41ef-9f32-7f938995f85e
=> Waiting to start installation
=> Registering subdomain .
=> Verifying manifest .
=> Downloading image ..............
=> Creating volume .
=> Creating container
=> Setting up collectd profile ................
=> Waiting for DNS propagation ...
App is installed.
```
This makes the app available at https://tutorial-craft.selfhost.io.
Open the app in your default browser:
```
cloudron open
```
You should see `Hello World`.
# Testing
The application testing cycle involves `cloudron build` and `cloudron install`.
Note that `cloudron install` updates an existing app in place.
You can view the logs using `cloudron logs`. When the app is running you can follow the logs
using `cloudron logs -f`.
For example, you can see the console.log output in our server.js with the command below:
```
$ cloudron logs
Using cloudron craft.selfhost.io
2015-05-08T03:28:40.233940616Z Server running at port 8000
```
It is also possible to run a *shell* and *execute* arbitrary commands in the context of the application
process by using `cloudron exec`. By default, exec simply drops you into an interactive bash shell with
which you can inspect the file system and the environment.
```
$ cloudron exec
```
You can also execute arbitrary commands:
```
$ cloudron exec env # display the env variables that your app is running with
```
# Storing data
For file system storage, an app can use the `localstorage` addon to store data under `/app/data`.
When the `localstorage` addon is active, any data under /app/data is automatically backed up. When an
app is updated, /app/data already contains the data generated by the previous version.
*Note*: For convenience, the initial CloudronManifest.json generated by `cloudron init` already contains this
addon.
Let us put this theory into action by saving a *visit counter* as a file.
*server.js* has been modified to count the number of visitors on the site by storing a counter
in a file named ```counter.dat```.
File ```tutorial/server.js```
```javascript
var http = require('http'),
fs = require('fs'),
util = require('util');
var COUNTER_FILE = '/app/data/counter.dat';
var server = http.createServer(function (request, response) {
var counter = 0;
if (fs.existsSync(COUNTER_FILE)) {
// read existing counter if it exists
counter = parseInt(fs.readFileSync(COUNTER_FILE, 'utf8'), 10);
}
response.writeHead(200, {"Content-Type": "text/plain"});
response.end(util.format("Hello World. %s visitors have visited this page\n", counter));
++counter; // bump the counter
fs.writeFileSync(COUNTER_FILE, counter + '', 'utf8'); // save back counter
});
server.listen(8000);
console.log("Server running at port 8000");
```
Now every time you refresh the page you will notice that the counter bumps up. You will
also notice that if you make changes to the app and do a `cloudron install`, the `counter.dat`
is *retained* across updates.
# Database
Most web applications require a database of some form. In theory, it is possible to run any
database you want as part of the application image. This is, however, a waste of server resources
should every app runs it's own database server.
To solve this, the Cloudron provides shareable resources like databases in form of ```addons```.
The database server is managed by the Cloudron and the application simply needs to request access to
the database in the CloudronManifest.json. While the database server itself is a shared resource, the
databases are exclusive to the application. Each database is password protected and accessible only
to the application. Databases and tables can be configured without restriction as the application
requires.
Cloudron currently provides `mysql`, `postgresql`, `mongodb`, `redis` database addons.
For this tutorial, let us try to save the counter in `redis` addon. For this, we make use of the
[redis](https://www.npmjs.com/package/redis) module.
Since this is a node.js app, let's add a very basic `package.json` containing the `redis` module dependency.
File `tutorial/package.json`
```json
{
"name": "tutorial",
"version": "1.0.0",
"dependencies": {
"redis": "^0.12.1"
}
}
```
and modify our Dockerfile to look like this:
File `tutorial/Dockerfile`
```dockerfile
FROM cloudron/base:0.9.0
ADD server.js /app/code/server.js
ADD package.json /app/code/package.json
WORKDIR /app/code
RUN npm install --production
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
```
Notice the new `RUN` command which installs the node module dependencies in package.json using `npm install`.
Since we want to use redis, we have to modify the CloudronManifest.json to make redis available for this app.
File `tutorial/CloudronManifest.json`
```json
{
"id": "io.cloudron.tutorial",
"author": "John Doe",
"title": "Tutorial App",
"description": "App that uses node.js",
"tagline": "Changing the world one app at a time",
"version": "0.0.1",
"healthCheckPath": "/",
"httpPort": 8000,
"addons": {
"localstorage": {},
"redis": {}
},
"minBoxVersion": "0.0.1",
"manifestVersion": 1,
"website": "https://cloudron.io",
"contactEmail": "support@cloudron.io",
"icon": "",
"mediaLinks": []
}
```
When the application runs, environment variables `REDIS_HOST`, `REDIS_PORT` and
`REDIS_PASSWORD` are injected. You can read about the environment variables in the
[Redis reference](/references/addons.html#redis).
Let's change `server.js` to use redis instead of file backed counting:
File ```tutorial/server.js```
```javascript
var http = require('http'),
fs = require('fs'),
util = require('util'),
redis = require('redis');
var redisClient = redis.createClient(process.env.REDIS_PORT, process.env.REDIS_HOST);
redisClient.auth(process.env.REDIS_PASSWORD);
redisClient.on("error", function (err) {
console.log("Redis Client Error " + err);
});
var COUNTER_KEY = 'counter';
var server = http.createServer(function (request, response) {
redisClient.get(COUNTER_KEY, function (err, reply) {
var counter = (!err && reply) ? parseInt(reply, 10) : 0;
response.writeHead(200, {"Content-Type": "text/plain"});
response.end(util.format("Hello World. %s visitors have visited this page\n", counter));
redisClient.incr(COUNTER_KEY);
});
});
server.listen(8000);
console.log("Server running at port 8000");
```
Simply `cloudron build` and `cloudron install` to test your app!
# Authentication
The Cloudron has a centralized panel for managing users and groups. Apps can integrate Single Sign-On
authentication using LDAP or OAuth.
Note that apps that are single user can skip Single Sign-On support. The Cloudron implements an `OAuth
proxy` (accessed through the app configuration dialog) that optionally lets the Cloudron admin make the
app visible only for logged in users.
## LDAP
Let's start out by adding the [ldap](/references/addons.html#ldap) addon to the manifest.
File `tutorial/CloudronManifest.json`
```json
{
"id": "io.cloudron.tutorial",
"author": "John Doe",
"title": "Tutorial App",
"description": "App that uses node.js",
"tagline": "Changing the world one app at a time",
"version": "0.0.1",
"healthCheckPath": "/",
"httpPort": 8000,
"addons": {
"localstorage": {},
"ldap": {}
},
"minBoxVersion": "0.0.1",
"manifestVersion": 1,
"website": "https://cloudron.io",
"contactEmail": "support@cloudron.io",
"icon": "",
"mediaLinks": []
}
```
Building and installing the app shows that the app gets new LDAP specific environment variables.
```
$ cloudron build
$ cloudron install
$ cloudron exec env | grep LDAP
LDAP_SERVER=172.17.42.1
LDAP_PORT=3002
LDAP_URL=ldap://172.17.42.1:3002
LDAP_USERS_BASE_DN=ou=users,dc=cloudron
LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron
```
Let's test the environment variables to use by using the [ldapjs](http://www.ldapjs.org) npm module.
We start by adding ldapjs to package.json.
File `tutorial/package.json`
```json
{
"name": "tutorial",
"version": "1.0.0",
"dependencies": {
"ldapjs": "^0.7.1"
}
}
```
The server code has been modified to authenticate using the `X-Username` and `X-Password` headers for
any path other than '/'.
File `tutorial/server.js`
```javascript
var http = require("http"),
ldap = require('ldapjs');
var ldapClient = ldap.createClient({ url: process.env.LDAP_URL });
var server = http.createServer(function (request, response) {
if (request.url === '/') {
response.writeHead(200, {"Content-Type": "text/plain"});
return response.end();
}
var username = request.headers['x-username'] || '';
var password = request.headers['x-password'] || '';
var ldapDn = 'cn=' + username + ',' + process.env.LDAP_USERS_BASE_DN;
ldapClient.bind(ldapDn, password, function (error) {
if (error) {
response.writeHead(401, {"Content-Type": "text/plain"});
response.end('Failed to authenticate: ' + error);
} else {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end('Successfully authenticated');
}
});
});
server.listen(8000);
console.log("Server running at port 8000");
```
Once we have used `cloudron build` and `cloudron install`, you can use `curl` to test
credentials as follows:
```bash
# Test with various credentials here. Your cloudon admin username and password should succeed.
curl -X 'X-Username: admin' -X 'X-Password: pass' https://tutorial-craft.selfhost.io/login
```
## OAuth
An app can integrate with OAuth 2.0 Authorization code grant flow by adding
[oauth](/references/addons.html#oauth) to CloudronManifest.json `addons` section.
Doing so will get the following environment variables:
```
$ cloudron exec env
OAUTH_CLIENT_ID=cid-addon-4089f65a-2adb-49d2-a6d1-e519b7d85e8d
OAUTH_CLIENT_SECRET=5af99a9633283aa15f5e6df4a108ff57f82064e4845de8bce8ad3af54dfa9dda
OAUTH_ORIGIN=https://my-craft.selfhost.io
API_ORIGIN=https://my-craft.selfhost.io
HOSTNAME=tutorial-craft.selfhost.io
```
OAuth Authorization code grant flow works as follows:
* App starts the flow by redirecting the user to Cloudron authorization endpoint of the following format:
```
https://API_ORIGIN/api/v1/oauth/dialog/authorize?response_type=code&client_id=OAUTH_CLIENT_ID&redirect_uri=CALLBACK_URL&scope=profile
```
In the above URL, API_ORIGIN and OAUTH_CLIENT_ID are environment variables. CALLBACK_URL is a url of the app
to which the user will be redirected back to after successful authentication. CALLBACK_URL has to have the
same origin as the app.
* The Cloudron OAuth server authenticates the user (using a password form) at the above URL. It also establishes
that the user grants the client's access request.
* If the user authenticated successfully, it will redirect the browser to CALLBACK_URL with a `code` query parameter.
* The app can exchange the `code` above for a `access token` by using the `OAUTH_CLIENT_SECRET`. It does so by making
a _POST_ request to the following url:
```
https://API_ORIGIN/api/v1/oauth/token?response_type=token&client_id=OAUTH_CLIENT_ID
```
with the following request body (json):
```json
{
"grant_type": "authorization_code",
"code": "<the code received in CALLBACK_URL query parameter>",
"redirect_uri": "https://<HOSTNAME>",
"client_id": "<OAUTH_CLIENT_ID>",
"client_secret": "<OAUTH_CLIENT_SECRET>"
}
```
In the above URL, API_ORIGIN, OAUTH_CLIENT_ID and HOSTNAME are environment variables. The response contains
the `access_token` in the body.
* The `access_token` can be used to get the [user's profile](/references/api.html#profile) using the following url:
```
https://API_ORIGIN/api/v1/profile?access_token=ACCESS_TOKEN
```
The `access_token` may also be provided in the `Authorization` header as `Bearer: <token>`.
An implementation of the above OAuth logic is at [ircd-app](https://github.com/cloudron-io/ircd-app/blob/master/settings/app.js).
The following libraries implement Cloudron OAuth for Ruby and Javascript.
* [omniauth-cloudron](https://github.com/cloudron-io/omniauth-cloudron)
* [passport-cloudron](https://github.com/cloudron-io/passport-cloudron)
# Beta Testing
Once your app is ready, you can upload it to the store for `beta testing` by
other Cloudron users. This can be done using:
```
cloudron upload
```
The app should now be visible in the Store view of your cloudron under
the 'Testing' section. You can check if the icon, description and other details
appear correctly.
Other Cloudron users can install your app on their Cloudron's using
`cloudron install --appstore-id <appid@version>`. Note that this currently
requires your beta testers to install the CLI tool and put their Cloudron in
developer mode.
# Publishing
Once you are satisfied with the beta testing, you can submit it for review.
```
cloudron submit
```
The cloudron.io team will review the app and publish the app to the store.
# Next steps
Congratulations! You are now well equipped to build web applications for the Cloudron.
# Samples
* [Lets Chat](https://github.com/cloudron-io/letschat-app)
* [Haste bin](https://github.com/cloudron-io/haste-app)
* [Pasteboard](https://github.com/cloudron-io/pasteboard-app)
+483
View File
@@ -0,0 +1,483 @@
# Overview
This tutorial outlines how to package an existing web application for the Cloudron.
If you are aware of Docker and Heroku, you should feel at home packaging for the
Cloudron. Roughly, the steps involved are:
* Create a Dockerfile for your application. If your application already has
a Dockerfile, you should able to reuse most of it. By virtue of Docker, the Cloudron
is able to run apps written in any language/framework.
* Create a CloudronManifest.json that provides information like title, author, description
etc. You can also specify the addons (like database) required
to run your app. When the app runs on the Cloudron, it will have environment
variables set for connecting to the addon.
* Test the app on your Cloudron with the CLI tool.
* Optionally, submit the app to [Cloudron Store](/appstore.html).
# Prerequisites
## Install CLI tool
The Cloudron CLI tool allows you to install, configure and test apps on your Cloudron.
Installing the CLI tool requires [node.js](https://nodejs.org/) and
[npm](https://www.npmjs.com/). You can then install the CLI tool using the following
command:
```
sudo npm install -g cloudron
```
Note: Depending on your setup, you can run the above command without `sudo`.
## Login to Cloudron
The `cloudron` command should now be available in your path.
You can login to your Cloudron now:
```
$ cloudron login
Cloudron Hostname: craft.selfhost.io
Enter credentials for craft.selfhost.io:
Username: girish
Password:
Login successful.
```
# Basic app
We will first package a very simple app to understand how the packaging works.
You can clone this app from https://git.cloudron.io/cloudron/tutorial-basic.
## The server
The basic app server is a very simple HTTP server that runs on port 8000.
While the server in this tutorial uses node.js, you can write your server
in any language you want.
```server.js
var http = require("http");
var server = http.createServer(function (request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello World\n");
});
server.listen(8000);
console.log("Server running at port 8000");
```
## Dockerfile
The Dockerfile contains instructions on how to create an image for your application.
```Dockerfile
FROM cloudron/base:0.9.0
ADD server.js /app/code/server.js
CMD [ "/usr/local/node-4.2.1/bin/node", "/app/code/server.js" ]
```
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
All Cloudron apps **must** start from this base image. This approach conserves space on the Cloudron since
Docker images tend to be quiet large.
The `ADD` command copies the source code of the app into the directory `/app/code`. There is nothing special
about the `/app/code` directory and it is merely a convention we use to store the application code.
The `CMD` command specifies how to run the server. The base image already contains many different versions of
node.js. We use Node 4.2.1 here.
This Dockerfile can be built and run locally as:
```
docker build -t tutorial .
docker run -p 8000:8000 -ti tutorial
```
## Manifest
The `CloudronManifest.json` specifies
* Information for installing and running the app on the Cloudron. This includes fields like addons, httpPort, tcpPorts.
* Information about displaying the app on the Cloudron Store. For example, fields like title, author, description.
Create the CloudronManifest.json using `cloudron init` as follows:
```
$ cloudron init
id: io.cloudron.tutorial # unique id for this app. use reverse domain name convention
author: John Doe # developer or company name of the for user <email>
title: Tutorial App # Cloudron Store title of this app
description: App that uses node.js # A string or local file reference like file://DESCRIPTION.md
tagline: Changing the world one app at a time # A tag line for this app for the Cloudron Store
website: https://cloudron.io # A link to this app's website
contactEmail: support@cloudron.io # Contact email of developer or company
httPort: 8000 # The http port on which this application listens to
```
The above command creates a CloudronManifest.json:
File ```tutorial/CloudronManifest.json```
```json
{
"id": "io.cloudron.tutorial",
"title": "Tutorial App",
"author": "John Doe",
"description": "file://DESCRIPTION.md",
"changelog": "file://CHANGELOG",
"tagline": "Changing the world one app at a time",
"version": "0.0.1",
"healthCheckPath": "/",
"httpPort": 8000,
"addons": {
"localstorage": {}
},
"manifestVersion": 1,
"website": "https://cloudron.io",
"contactEmail": "support@cloudron.io",
"icon": "",
"tags": [
"changme"
],
"mediaLinks": [ ]
}
```
You can read in more detail about each field in the [Manifest reference](/references/manifest.html). The
`localstorage` addon allows the app to store files in `/app/data`. We will explore addons further further
down in this tutorial.
Additional files created by `init` are:
* `DESCRIPTION.md` - A markdown file providing description of the app for the Cloudron Store.
* `CHANGELOG` - A file containing change information for each version released to the Cloudron Store. This
information is shown when the user updates the app.
# Installing
We now have all the necessary files in place to build and deploy the app to the Cloudron.
## Building
Building, pushing and pulling docker images can be very bandwidth and CPU intensive. To alleviate this
problem, apps are built using the `build service` which uses `cloudron.io` account credentials.
**Warning**: As of this writing, the build service uses the public Docker registry and the images that are built
can be downloaded by anyone. This means that your source code will be viewable by others.
Initiate a build using ```cloudron build```:
```
$ cloudron build
Building io.cloudron.tutorial@0.0.1
Appstore login:
Email: ramakrishnan.girish@gmail.com # cloudron.io account
Password: # Enter password
Login successful.
Build scheduled with id e7706847-f2e3-4ba2-9638-3f334a9453a5
Waiting for build to begin, this may take a bit...
Downloading source
Building
Step 1 : FROM cloudron/base:0.9.0
---> be9fc6312b2d
Step 2 : ADD server.js /app/code/server.js
---> 10513e428d7a
Removing intermediate container 574573f6ed1c
Step 3 : CMD /usr/local/node-4.2.1/bin/node /app/code/server.js
---> Running in b541d149b6b9
---> 51aa796ea6e5
Removing intermediate container b541d149b6b9
Successfully built 51aa796ea6e5
Pushing
The push refers to a repository [docker.io/cloudron/img-062037096d69bbf3ffb5b9316ad89cb9] (len: 1)
Pushed 51aa796ea6e5
Pushed 10513e428d7a
Image already exists be9fc6312b2d
Image already exists a0261a2a7c75
Image already exists f9d4f0f1eeed
Image already exists 2b650158d5d8
e7706847-f2e3-4ba2-9638-3f334a9453a5: digest: sha256:8241d68b65874496191106ecf2ee8f3df2e05a953cd90ff074a6f8815a49389c size: 26098
Build succeeded
Success
```
## Installing
Now that we have built the image, we can install our latest build on the Cloudron
using the following command:
```
$ cloudron install
Using cloudron craft.selfhost.io
Using build 76cebfdd-7822-4f3d-af17-b3eb393ae604 from 1 hour ago
Location: tutorial # This is the location into which the application installs
App is being installed with id: 4dedd3bb-4bae-41ef-9f32-7f938995f85e
=> Waiting to start installation
=> Registering subdomain .
=> Verifying manifest .
=> Downloading image ..............
=> Creating volume .
=> Creating container
=> Setting up collectd profile ................
=> Waiting for DNS propagation ...
App is installed.
```
Open the app in your default browser:
```
cloudron open
```
You should see `Hello World`.
# Testing
The application testing cycle involves `cloudron build` and `cloudron install`.
Note that `cloudron install` updates an existing app in place.
You can view the logs using `cloudron logs`. When the app is running you can follow the logs
using `cloudron logs -f`.
For example, you can see the console.log output in our server.js with the command below:
```
$ cloudron logs
Using cloudron craft.selfhost.io
16:44:11 [main] Server running at port 8000
```
It is also possible to run a *shell* and *execute* arbitrary commands in the context of the application
process by using `cloudron exec`. By default, exec simply drops you into an interactive bash shell with
which you can inspect the file system and the environment.
```
$ cloudron exec
```
You can also execute arbitrary commands:
```
$ cloudron exec env # display the env variables that your app is running with
```
### DevelopmentMode
When debugging complex startup scripts, one can specify `"developmentMode": true,` in the CloudronManifest.json.
This will ignore the `RUN` command, specified in the Dockerfile and allows the developer to interactively test
the startup scripts using `cloudron exec`.
**Note:** that an app running in this mode has full read/write access to the filesystem and all memory limits are lifted.
# Addons
## Filesystem
The application container created on the Cloudron has a `readonly` file system. Writing to any location
other than the below will result in an error:
* `/tmp` - Use this location for temporary files. The Cloudron will cleanup any files in this directory
periodically.
* `/run` - Use this location for runtime configuration and dynamic data. These files should not be expected
to persist across application restarts (for example, after an update or a crash).
* `/app/data` - Use this location to store application data that is to be backed up. To use this location,
you must use the [localstorage](/references/addons.html#localstorage) addon. For convenience, the initial CloudronManifest.json generated by
`cloudron init` already contains this addon.
## Database
Most web applications require a database of some form. In theory, it is possible to run any
database you want as part of the application image. This is, however, a waste of server resources
should every app runs it's own database server.
Cloudron currently provides [mysql](/references/addons.html#mysql), [postgresql](/references/addons.html#postgresql),
[mongodb](/references/addons.html#mongodb), [redis](/references/addons.html#redis) database addons. When choosing
these addons, the Cloudron will inject environment variables that contain information on how to connect
to the addon.
See https://git.cloudron.io/cloudron/tutorial-redis for a simple example of how redis can be used by
an application. The server simply uses the environment variables to connect to redis.
## Email
Cloudron applications can send email using the `sendmail` addon. Using the `sendmail` addon provides
the SMTP server and authentication credentials in environment variables.
Cloudron applications can also receive mail via IMAP using the `recvmail` addon.
## Authentication
The Cloudron has a centralized panel for managing users and groups. Apps can integrate Single Sign-On
authentication using LDAP or OAuth.
Apps can integrate with the Cloudron authentication system using LDAP, OAuth or Simple Auth. See the
[authentication](/references/authentication.html) reference page for more details.
See https://git.cloudron.io/cloudron/tutorial-ldap for a simple example of how to authenticate via LDAP.
For apps that are single user can skip Single Sign-On support by setting the `"singleUser": true`
in the manifest. By doing so, the Cloudron will installer will show a dialog to choose a user.
For app that have no user management at all, the Cloudron implements an `OAuth proxy` that
optionally lets the Cloudron admin make the app visible only for logged in users.
# Best practices
## No Setup
A Cloudron app is meant to instantly usable after installation. For this reason, Cloudron apps must not
show any setup screen after installation and should simply choose reasonable defaults.
Databases, email configuration should be automatically picked up from the environment variables using
addons.
## Dockerfile
The app is run as a read-only docker container. Because of this:
* Install any required packages in the Dockerfile.
* Create static configuration files in the Dockerfile.
* Create symlinks to dynamic configuration files under /run in the Dockerfile.
## Process manager
Docker supports restarting processes natively. Should your application crash, it will be restarted
automatically. If your application is a single process, you do not require any process manager.
Use supervisor, pm2 or any of the other process managers if you application has more then one component.
This **excludes** web servers like apache, nginx which can already manage their children by themselves.
Be sure to pick a process manager that forwards signals to child processes.
## Automatic updates
Some apps support automatic updates by overwriting themselves. A Cloudron app cannot overwrite itself
because of the read-only file system. For this reason, disable auto updates for app and let updates be
triggered through the Cloudron Store. This ties in better to the Cloudron's update and restore approach
should something go wrong with the update.
## Logging
Cloudron applications stream their logs to stdout and stderr. In practice, this ideal is hard to achieve.
Some programs like apache simply don't log to stdout. In those cases, simply log to `/tmp` or `/run`.
Logging to stdout has many advantages:
* App does not need to rotate logs and the Cloudron takes care of managing logs.
* App does not need special mechanism to release log file handles (on a log rotate).
* Integrates better with tooling like cloudron cli.
## Memory
By default, applications get 256MB RAM (including swap). This can be changed using the `memoryLimit`
field in the manifest.
Design your application runtime for concurrent use by 50 users. The Cloudron is not designed for
concurrent access by 100s or 1000s of users.
## Authentication
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
This saves the user from having to manage separate set of credentials for each app.
## Startup Script
Many apps do not launch the server directly, as we did in our basic example. Instead, they execute
a `start.sh` script (named so by convention) which launches the server. Before starting the server,
the `start.sh` script does the following:
* When using the `localstorage` addon, it changes the ownership of files in `/app/data` as desired using `chown`. This
is necessary because file permissions may not be correctly preserved across backup, restore, application and base image
updates.
* Addon information (mail, database) exposed as environment are subject to change across restarts and an application
must use these values directly (i.e not cache them across restarts). For this reason, it usually regenerates
any config files with the current database settings on each invocation.
* Finally, it starts the server as a non-root user.
The app's main process must handle SIGTERM and forward it as required to child processes. bash does not
automatically forward signals to child processes. For this reason, when using a startup shell script,
remember to use exec <app> as the last line. Doing so will replace bash with your program and allows
your program to handle signals as required.
# Beta Testing
Once your app is ready, you can upload it to the store for `beta testing` by
other Cloudron users. This can be done using:
```
cloudron upload
```
The app should now be visible in the Store view of your cloudron under
the 'Testing' section. You can check if the icon, description and other details
appear correctly.
Other Cloudron users can install your app on their Cloudron's using
`cloudron install --appstore-id <appid@version>`.
# Publishing
Once you are satisfied with the beta testing, you can submit it for review.
```
cloudron submit
```
The cloudron.io team will review the app and publish the app to the store.
# Updating the app
## Versioning
To create an update for an app, simply bump up the [semver version](/references/manifest.html#version) field in
the manifest and publish a new version to the store.
The Cloudron chooses the next app version to update to based on the following algorithm:
* Choose the maximum `patch` version matching the app's current `major` and `minor` version.
* Failing the above, choose the maximum patch version of the next minor version matching the app's current `major` version.
* Failing the above, choose the maximum patch and minor version of the next major version
For example, let's assume the versions 1.1.3, 1.1.4, 1.1.5, 1.2.4, 1.2.6, 1.3.0, 2.0.0 are published.
* If the app is running 1.1.3, then app will directly update to 1.1.5 (skipping 1.1.4)
* Once in 1.1.5, the app will update to 1.2.6 (skipping 1.2.4)
* Once in 1.2.6, the app will update to 1.3.0
* Once in 1.3.0, the app will update to 2.0.0
The Cloudron admins get notified by email for any major or minor app releases.
## Failed updates
The Cloudron always makes a backup of the app before making an update. Should the
update fail, the user can restore to the backup (which will also restore the app's
code to the previous version).
# Cloudron Button
The [Cloudron Button](/references/button.html) allows anyone to install your application with the click of a button
on their Cloudron.
The button can be added to just about any website including the application's website
and README.md files in GitHub repositories.
# Next steps
Congratulations! You are now well equipped to build web applications for the Cloudron.
You can see some examples of how real apps are packaged here:
* [Lets Chat](https://git.cloudron.io/cloudron/letschat-app)
* [Haste bin](https://git.cloudron.io/cloudron/haste-app)
* [Pasteboard](https://git.cloudron.io/cloudron/pasteboard-app)
+3 -2
View File
@@ -10,7 +10,7 @@ var ejs = require('gulp-ejs'),
serve = require('gulp-serve'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
minifyCSS = require('gulp-minify-css'),
cssnano = require('gulp-cssnano'),
autoprefixer = require('gulp-autoprefixer'),
argv = require('yargs').argv;
@@ -22,6 +22,7 @@ gulp.task('3rdparty', function () {
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
@@ -119,7 +120,7 @@ gulp.task('css', function () {
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(minifyCSS())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
-159
View File
@@ -1,159 +0,0 @@
#!/bin/bash
set -eu -o pipefail
echo ""
echo "======== Cloudron Installer ========"
echo ""
if [ $# -lt 4 ]; then
echo "Usage: ./installer.sh <fqdn> <aws key id> <aws key secret> <bucket> <provider> <revision>"
exit 1
fi
# commandline arguments
readonly fqdn="${1}"
readonly aws_access_key_id="${2}"
readonly aws_access_key_secret="${3}"
readonly aws_backup_bucket="${4}"
readonly provider="${5}"
readonly revision="${6}"
# environment specific urls
readonly api_server_origin="https://api.dev.cloudron.io"
readonly web_server_origin="https://dev.cloudron.io"
readonly release_bucket_url="https://s3.amazonaws.com/dev-cloudron-releases"
readonly versions_url="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
# runtime consts
readonly installer_code_file="/tmp/box.tar.gz"
readonly installer_tmp_dir="/tmp/box"
readonly cert_folder="/tmp/certificates"
# check for fqdn in /ets/hosts
echo "[INFO] checking for hostname entry"
readonly hostentry_found=$(grep "${fqdn}" /etc/hosts || true)
if [[ -z $hostentry_found ]]; then
echo "[WARNING] No entry for ${fqdn} found in /etc/hosts"
echo "Adding an entry ..."
cat >> /etc/hosts <<EOF
# The following line was added by the Cloudron installer script
127.0.1.1 ${fqdn} ${fqdn}
EOF
else
echo "Valid hostname entry found in /etc/hosts"
fi
echo ""
echo "[INFO] ensure minimal dependencies ..."
apt-get update
apt-get install -y curl
echo ""
echo "[INFO] Generating certificates ..."
rm -rf "${cert_folder}"
mkdir -p "${cert_folder}"
cat > "${cert_folder}/CONFIG" <<EOF
[ req ]
default_bits = 1024
default_keyfile = keyfile.pem
distinguished_name = req_distinguished_name
prompt = no
req_extensions = v3_req
[ req_distinguished_name ]
C = DE
ST = Berlin
L = Berlin
O = Cloudron UG
OU = Cloudron
CN = ${fqdn}
emailAddress = cert@cloudron.io
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = ${fqdn}
DNS.2 = *.${fqdn}
EOF
# generate cert files
openssl genrsa 2048 > "${cert_folder}/host.key"
openssl req -new -out "${cert_folder}/host.csr" -key "${cert_folder}/host.key" -config "${cert_folder}/CONFIG"
openssl x509 -req -days 3650 -in "${cert_folder}/host.csr" -signkey "${cert_folder}/host.key" -out "${cert_folder}/host.cert" -extensions v3_req -extfile "${cert_folder}/CONFIG"
# make them json compatible, by collapsing to one line
tls_cert=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.cert")
tls_key=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.key")
echo ""
echo "[INFO] Fetching installer code ..."
curl "${installer_code_url}" -o "${installer_code_file}"
echo ""
echo "[INFO] Extracting installer code to ${installer_tmp_dir} ..."
rm -rf "${installer_tmp_dir}" && mkdir -p "${installer_tmp_dir}"
tar xvf "${installer_code_file}" -C "${installer_tmp_dir}"
echo ""
echo "Creating initial provisioning config ..."
cat > /root/provision.json <<EOF
{
"sourceTarballUrl": "",
"data": {
"apiServerOrigin": "${api_server_origin}",
"webServerOrigin": "${web_server_origin}",
"fqdn": "${fqdn}",
"token": "",
"isCustomDomain": true,
"boxVersionsUrl": "${versions_url}",
"version": "",
"tlsCert": "${tls_cert}",
"tlsKey": "${tls_key}",
"provider": "${provider}",
"backupConfig": {
"provider": "s3",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}",
"bucket": "${aws_backup_bucket}",
"prefix": "backups"
},
"dnsConfig": {
"provider": "route53",
"accessKeyId": "${aws_access_key_id}",
"secretAccessKey": "${aws_access_key_secret}"
},
"tlsConfig": {
"provider": "letsencrypt-dev"
}
}
}
EOF
echo "[INFO] Running Ubuntu initializing script ..."
/bin/bash "${installer_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${revision}" selfhosting
echo ""
echo "[INFO] Reloading systemd daemon ..."
systemctl daemon-reload
echo ""
echo "[INFO] Restart docker ..."
systemctl restart docker
echo ""
echo "[FINISHED] Now starting Cloudron init jobs ..."
systemctl start box-setup
# TODO this is only for convenience we should probably just let the user do a restart
sleep 5 && sync
systemctl start cloudron-installer
journalctl -u cloudron-installer.service -f
+518 -142
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@
"json": "^9.0.3",
"morgan": "^1.5.1",
"proxy-middleware": "^0.15.0",
"request": "^2.72.0",
"safetydance": "0.0.19",
"semver": "^5.1.0",
"superagent": "^0.21.0"
+3 -2
View File
@@ -4,12 +4,13 @@ set -eu -o pipefail
readonly BOX_SRC_DIR=/home/yellowtent/box
readonly DATA_DIR=/home/yellowtent/data
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="${script_dir}/../../node_modules/.bin/json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 180"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 300"
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
# create a provision file for testing. %q escapes args. %q is reused as much as necessary to satisfy $@
(echo -e "#!/bin/bash\n"; printf "%q " "${script_dir}/installer.sh" "$@") > /home/yellowtent/provision.sh
+43 -5
View File
@@ -16,6 +16,7 @@ var assert = require('assert'),
json = require('body-parser').json,
lastMile = require('connect-lastmile'),
morgan = require('morgan'),
request = require('request'),
superagent = require('superagent');
exports = module.exports = {
@@ -29,22 +30,59 @@ var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
var gHttpServer = null; // update server; used for updates
function provisionDigitalOcean(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
superagent.get('http://169.254.169.254/metadata/v1.json').end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new Error('Error getting metadata'));
}
var userData = JSON.parse(result.body.user_data);
callback(null, JSON.parse(result.body.user_data));
});
}
function provisionEC2(callback) {
// need to use request, since octet-stream data
request('http://169.254.169.254/latest/user-data', { timeout: 5000 }, function (error, response, body) {
if (error || response.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new Error('Error getting metadata'));
}
callback(null, JSON.parse(body));
});
}
function provision(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) {
debug('provision: already provisioned');
return callback(null); // already provisioned
}
async.retry({ times: 5, interval: 30000 }, function (done) {
// try first digitalocean, then ec2
provisionDigitalOcean(function (error1, userData) {
if (!error1) return done(null, userData);
provisionEC2(function (error2, userData) {
if (!error2) return done(null, userData);
console.error('Unable to get meta data: ', error1.message + ' ' + error2.message);
callback(new Error(error1.message + ' ' + error2.message));
});
});
}, function (error, userData) {
if (error) return callback(error);
installer.provision(userData, callback);
});
}
function provisionLocal(callback) {
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) {
debug('provisionLocal: already provisioned');
return callback(null); // already provisioned
}
if (!fs.existsSync(PROVISION_CONFIG_FILE)) {
console.error('No provisioning data found at %s', PROVISION_CONFIG_FILE);
@@ -122,7 +160,7 @@ function start(callback) {
actions = [
startUpdateServer,
provisionDigitalOcean
provision
];
}
+11 -19
View File
@@ -4,7 +4,6 @@ set -eu -o pipefail
readonly USER_HOME="/home/yellowtent"
readonly APPS_SWAP_FILE="/apps.swap"
readonly BACKUP_SWAP_FILE="/backup.swap" # used when doing app backups
readonly USER_DATA_FILE="/root/user_data.img"
readonly USER_DATA_DIR="/home/yellowtent/data"
@@ -17,14 +16,15 @@ if [[ -b "/dev/xvda1" ]]; then
disk_device="/dev/xvda1"
fi
# allow root access over ssh
sed -e 's/.* \(ssh-rsa.*\)/\1/' -i /root/.ssh/authorized_keys
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly swap_size="${physical_memory}"
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
readonly app_count=$((${physical_memory} / 200)) # estimated app count
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
readonly disk_size=$((disk_size_gb * 1024))
readonly backup_swap_size=1024
# readonly system_size=5120 # 5 gigs for system libs, installer, box code and tmp
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
@@ -33,8 +33,7 @@ echo "Physical memory: ${physical_memory}"
echo "Estimated app count: ${app_count}"
echo "Disk size: ${disk_size}"
# Allocate two sets of swap files - one for general app usage and another for backup
# The backup swap is setup for swap on the fly by the backup scripts
# Allocate swap for general app usage
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
echo "Creating Apps swap file of size ${swap_size}M"
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
@@ -46,20 +45,13 @@ else
echo "Apps Swap file already exists"
fi
if [[ ! -f "${BACKUP_SWAP_FILE}" ]]; then
echo "Creating Backup swap file of size ${backup_swap_size}M"
fallocate -l "${backup_swap_size}m" "${BACKUP_SWAP_FILE}"
chmod 600 "${BACKUP_SWAP_FILE}"
mkswap "${BACKUP_SWAP_FILE}"
else
echo "Backups Swap file already exists"
fi
echo "Resizing data volume"
home_data_size=$((disk_size - system_size - swap_size - backup_swap_size - ext4_reserved))
home_data_size=$((disk_size - system_size - swap_size - ext4_reserved))
echo "Resizing up btrfs user data to size ${home_data_size}M"
umount "${USER_DATA_DIR}"
fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
mount "${USER_DATA_FILE}"
umount "${USER_DATA_DIR}" || true
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
btrfs filesystem resize max "${USER_DATA_DIR}"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN memoryLimit', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,21 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groups', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,22 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groupMembers', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,30 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see groups.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE backups(" +
"filename VARCHAR(128) NOT NULL," +
"creationTime TIMESTAMP," +
"version VARCHAR(128) NOT NULL," +
"type VARCHAR(16) NOT NULL," +
"dependsOn VARCHAR(4096)," +
"state VARCHAR(16) NOT NULL," +
"PRIMARY KEY (filename))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE backups', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE filename id VARCHAR(128)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE id filename VARCHAR(128)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) UNIQUE', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) NOT NULL UNIQUE', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
var dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = "CREATE TABLE eventlog(" +
"id VARCHAR(128) NOT NULL," +
"source JSON," +
"creationTime TIMESTAMP," +
"action VARCHAR(128) NOT NULL," +
"data JSON," +
"PRIMARY KEY (id))";
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE eventlog', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE mailboxes(' +
'name VARCHAR(128) NOT NULL,' +
'aliasTarget VARCHAR(128),' +
'creationTime TIMESTAMP,' +
'PRIMARY KEY (name))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mailboxes', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
// imports mailbox entries for existing users
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function addUserMailboxes(done) {
db.all('SELECT username FROM users', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (r, next) {
if (!r.username) return next();
db.runSql('INSERT INTO mailboxes (name) VALUES (?)', [ r.username ], next);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,16 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY installationProgress TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY installationProgress VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN xFrameOptions VARCHAR(512)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN xFrameOptions', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.all('SELECT id FROM users', function (error, results) {
if (error) return callback(error);
// existing cloudrons have email enabled by default. future cloudrons will have it disabled by default
var enable = results.length !== 0;
db.runSql('INSERT settings (name, value) VALUES("mail_config", ?)', [ JSON.stringify({ enabled: enable }) ], callback);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE * FROM settings WHERE name="mail_config"', [ ], callback);
};
+51 -8
View File
@@ -11,7 +11,7 @@
CREATE TABLE IF NOT EXISTS users(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) NOT NULL UNIQUE,
username VARCHAR(254) UNIQUE,
email VARCHAR(254) NOT NULL UNIQUE,
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
@@ -19,14 +19,26 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
showTutorial BOOLEAN DEFAULT 0,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
scope VARCHAR(512) NOT NULL,
expires BIGINT NOT NULL,
expires BIGINT NOT NULL, // FIXME: make this a timestamp
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS clients(
@@ -42,7 +54,7 @@ CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL,
installationState VARCHAR(512) NOT NULL,
installationProgress VARCHAR(512),
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
containerId VARCHAR(128),
@@ -50,14 +62,16 @@ CREATE TABLE IF NOT EXISTS apps(
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
accessRestrictionJson TEXT,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
oldConfigJson TEXT, // used to pass old config for apptask
oldConfigJson TEXT, // used to pass old config for apptask, can be removed when we use a queue
PRIMARY KEY(id));
@@ -72,7 +86,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
authCode VARCHAR(128) NOT NULL UNIQUE,
userId VARCHAR(128) NOT NULL,
clientId VARCHAR(128) NOT NULL,
expiresAt BIGINT NOT NULL,
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
PRIMARY KEY(authCode));
CREATE TABLE IF NOT EXISTS settings(
@@ -86,3 +100,32 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
filename VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn VARCHAR(4096), /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));
CREATE TABLE IF NOT EXISTS eventlog(
id VARCHAR(128) NOT NULL,
action VARCHAR(128) NOT NULL,
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data JSON, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
PRIMARY KEY (id));
+1259 -1369
View File
File diff suppressed because it is too large Load Diff
+15 -16
View File
@@ -14,13 +14,11 @@
],
"dependencies": {
"async": "^1.2.1",
"attempt": "^1.0.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.1.0",
"cloudron-manifestformat": "^2.2.0",
"cloudron-manifestformat": "^2.4.3",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "0.0.13",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
@@ -28,17 +26,18 @@
"csurf": "^1.6.6",
"db-migrate": "^0.9.2",
"debug": "^2.2.0",
"dockerode": "^2.2.2",
"dockerode": "^2.2.10",
"ejs": "^2.2.4",
"ejs-cli": "^1.0.1",
"ejs-cli": "^1.2.0",
"express": "^4.12.4",
"express-session": "^1.11.3",
"hat": "0.0.3",
"ini": "^1.3.4",
"json": "^9.0.3",
"ldapjs": "^0.7.1",
"memorystream": "^0.3.0",
"mime": "^1.3.4",
"morgan": "^1.6.0",
"moment-timezone": "^0.5.5",
"morgan": "^1.7.0",
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"native-dns": "^0.7.0",
@@ -48,6 +47,7 @@
"nodemailer-smtp-transport": "^1.0.3",
"oauth2orize": "^1.0.1",
"once": "^1.3.2",
"parse-links": "^0.1.0",
"passport": "^0.2.2",
"passport-http": "^0.2.2",
"passport-http-bearer": "^1.0.1",
@@ -55,18 +55,18 @@
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.0",
"safetydance": "^0.1.1",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.5.0",
"superagent": "^1.8.3",
"supererror": "^0.7.1",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"ursa": "^0.9.1",
"ursa": "^0.9.3",
"valid-url": "^1.0.9",
"validator": "^4.4.0",
"x509": "^0.2.2"
"validator": "^4.9.0",
"x509": "^0.2.4"
},
"devDependencies": {
"apidoc": "*",
@@ -77,8 +77,8 @@
"gulp": "^3.8.11",
"gulp-autoprefixer": "^2.3.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^1.0.0",
"gulp-minify-css": "^1.1.3",
"gulp-sass": "^2.0.1",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^1.5.2",
@@ -89,7 +89,6 @@
"mocha": "*",
"nock": "^3.4.0",
"node-sass": "^3.0.0-alpha.0",
"redis": "^2.4.2",
"request": "^2.65.0",
"sinon": "^1.12.2",
"yargs": "^3.15.0"
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=21
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.8.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
MONGODB_IMAGE=cloudron/mongodb:0.8.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.9.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
MYSQL_REPO=cloudron/mysql
POSTGRESQL_REPO=cloudron/postgresql
MONGODB_REPO=cloudron/mongodb
REDIS_REPO=cloudron/redis # if you change this, fix src/addons.js as well
MAIL_REPO=cloudron/mail
GRAPHITE_REPO=cloudron/graphite
+28 -9
View File
@@ -10,7 +10,8 @@ arg_fqdn=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire="false"
arg_retire_reason=""
arg_retire_info=""
arg_tls_config=""
arg_tls_cert=""
arg_tls_key=""
@@ -21,22 +22,40 @@ arg_backup_config=""
arg_dns_config=""
arg_update_config=""
arg_provider=""
arg_app_bundle=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire)
arg_retire="true"
shift
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# only read mandatory non-empty parameters here
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
EOF
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
arg_box_versions_url=$(echo "$2" | $json boxVersionsUrl)
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_app_bundle=$(echo "$2" | $json appBundle)
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey)
arg_token=$(echo "$2" | $json token)
+4 -1
View File
@@ -4,4 +4,7 @@
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connection=50
max_connections=50
# on ec2, without this we get a sporadic connection drop when doing the initial migration
max_allowed_packet=32M
-3
View File
@@ -25,9 +25,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
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 -1
View File
@@ -1,5 +1,5 @@
[Unit]
Description=Cloudron Smart Cloud
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service
@@ -7,7 +7,7 @@ StopWhenUnneeded=false
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
ExecStart="/home/yellowtent/box/crashnotifier.js" %I
ExecStart="/home/yellowtent/box/crashnotifierservice.js" %I
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
KillMode=process
User=yellowtent
+13 -8
View File
@@ -9,8 +9,6 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
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
echo "Setting up nginx update page"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
@@ -24,17 +22,24 @@ rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
infra_version="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${DATA_DIR}/nginx/applications/*
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
fi
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+32 -18
View File
@@ -21,7 +21,7 @@ source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used
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=$([[ -f "${CONFIG_DIR}/cloudron.conf" ]] && echo "true" || echo "false")
set_progress() {
local percent="$1"
@@ -34,12 +34,15 @@ set_progress() {
set_progress "1" "Create container"
$script_dir/container.sh
set_progress "5" "Adjust system settings"
hostnamectl set-hostname "${arg_fqdn}"
set_progress "10" "Ensuring directories"
# keep these in sync with paths.js
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
mkdir -p "${DATA_DIR}/box/acme" # acme keys
mkdir -p "${DATA_DIR}/graphite"
@@ -60,7 +63,13 @@ echo "Cleaning up snapshots"
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
# wait for all running mysql jobs
while true; do
if ! systemctl list-jobs | grep mysql; then break; fi
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
@@ -92,7 +101,7 @@ EOF
set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
service collectd restart
systemctl restart collectd
set_progress "30" "Setup nginx"
mkdir -p "${DATA_DIR}/nginx/applications"
@@ -107,7 +116,7 @@ if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
fi
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert"
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
@@ -119,12 +128,14 @@ else
fi
set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
chown "${USER}:${USER}" -R "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
# during updates, do not trample mail ownership behind the the mail container's back
find "${DATA_DIR}/box" -mindepth 1 -maxdepth 1 -not -path "${DATA_DIR}/box/mail" -print0 | xargs -0 chown -R "${USER}:${USER}"
chown "${USER}:${USER}" "${DATA_DIR}/box"
chown "${USER}:${USER}" -R "${DATA_DIR}/box/mail/dkim" # this is owned by box currently since it generates the keys
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" || true
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "40" "Setting up infra"
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
set_progress "65" "Creating cloudron.conf"
sudo -u yellowtent -H bash <<EOF
set -eu
@@ -138,15 +149,16 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"fqdn": "${arg_fqdn}",
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "admin@${arg_fqdn}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo},
"database": {
"hostname": "localhost",
"username": "root",
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
}
},
"appBundle": ${arg_app_bundle}
}
CONF_END
@@ -190,18 +202,20 @@ if [[ ! -z "${arg_tls_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
# Add webadmin oauth client
# The domain might have changed, therefor we have to update the record
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
echo "Add webadmin oauth cient"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
echo "Add webadmin api cient"
readonly ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"admin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
echo "Add localhost test oauth client"
ADMIN_SCOPES="root,developer,profile,users,apps,settings"
echo "Add SDK api client"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-sdk\", \"SDK\", \"built-in\", \"secret-sdk\", \"${admin_origin}\", \"*,roleSdk\")" box
echo "Add cli api client"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-cli\", \"Cloudron Tool\", \"built-in\", \"secret-cli\", \"${admin_origin}\", \"*,roleSdk\")" box
set_progress "80" "Starting Cloudron"
systemctl start cloudron.target
+7 -1
View File
@@ -18,11 +18,15 @@ server {
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
ssl_ciphers 'AES128+EECDH:AES128+EDH';
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -32,6 +36,7 @@ server {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Ssl on;
# upgrade is a hop-by-hop header (http://nginx.org/en/docs/http/websocket.html)
proxy_set_header Upgrade $http_upgrade;
@@ -58,6 +63,7 @@ server {
client_max_body_size 1m;
}
# the read timeout is between successive reads and not the whole connection
location ~ ^/api/v1/apps/.*/exec$ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 30m;
+12 -11
View File
@@ -24,7 +24,14 @@ http {
sendfile on;
keepalive_timeout 65;
# timeout for client to finish sending headers
client_header_timeout 30s;
# timeout for reading client request body (successive read timeout and not whole body!)
client_body_timeout 60s;
# keep-alive connections timeout in 65s. this is because many browsers timeout in 60 seconds
keepalive_timeout 65s;
# HTTP server
server {
@@ -50,22 +57,15 @@ http {
}
}
# We have to enable https for nginx to read in the vhost in http request
# and send a 404. This is a side-effect of using wildcard DNS
# This server handles the naked domain for custom domains.
# It can also be used for wildcard subdomain 404. This feature is not used by the Cloudron itself
# because box always sets up DNS records for app subdomains.
server {
listen 443 default_server;
ssl on;
ssl_certificate cert/host.cert;
ssl_certificate_key cert/host.key;
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# Disable check to allow unlimited body sizes
client_max_body_size 0;
error_page 404 = @fallback;
location @fallback {
internal;
@@ -79,6 +79,7 @@ http {
rewrite ^/$ /nakeddomain.html break;
}
# required for /api/v1/cloudron/avatar
location /api/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
-129
View File
@@ -1,129 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly DATA_DIR="/home/yellowtent/data"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
arg_fqdn="$1"
# removing containers ensures containers are launched with latest config updates
# restore code in appatask does not delete old containers
infra_version="none"
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
if [[ "${infra_version}" == "${INFRA_VERSION}" ]]; then
echo "Infrastructure is upto date"
exit 0
fi
echo "Upgrading infrastructure from ${infra_version} to ${INFRA_VERSION}"
existing_containers=$(docker ps -qa)
echo "Remove containers: ${existing_containers}"
if [[ -n "${existing_containers}" ]]; then
echo "${existing_containers}" | xargs docker rm -f
fi
# graphite
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
-m 75m \
--memory-swap 150m \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-v "${DATA_DIR}/graphite:/app/data" \
--read-only -v /tmp -v /run \
"${GRAPHITE_IMAGE}")
echo "Graphite container id: ${graphite_container_id}"
if docker images "${GRAPHITE_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${GRAPHITE_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old graphite images"
fi
# mail (MAIL_SMTP_PORT is 2500 in addons.js. used in mailer.js as well)
mail_container_id=$(docker run --restart=always -d --name="mail" \
-m 75m \
--memory-swap 150m \
-h "${arg_fqdn}" \
-e "DOMAIN_NAME=${arg_fqdn}" \
-v "${DATA_DIR}/box/mail:/app/data" \
--read-only -v /tmp -v /run \
"${MAIL_IMAGE}")
echo "Mail container id: ${mail_container_id}"
if docker images "${MAIL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MAIL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mail images"
fi
# mysql
mysql_addon_root_password=$(pwgen -1 -s)
docker0_ip=$(/sbin/ifconfig docker0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}')
cat > "${DATA_DIR}/addons/mysql_vars.sh" <<EOF
readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
readonly MYSQL_ROOT_HOST='${docker0_ip}'
EOF
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MYSQL_IMAGE}")
echo "MySQL container id: ${mysql_container_id}"
if docker images "${MYSQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MYSQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mysql images"
fi
# postgresql
postgresql_addon_root_password=$(pwgen -1 -s)
cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
EOF
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${POSTGRESQL_IMAGE}")
echo "PostgreSQL container id: ${postgresql_container_id}"
if docker images "${POSTGRESQL_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${POSTGRESQL_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old postgresql images"
fi
# mongodb
mongodb_addon_root_password=$(pwgen -1 -s)
cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
EOF
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
-m 100m \
--memory-swap 200m \
-h "${arg_fqdn}" \
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run \
"${MONGODB_IMAGE}")
echo "Mongodb container id: ${mongodb_container_id}"
if docker images "${MONGODB_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${MONGODB_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old mongodb images"
fi
# redis
if docker images "${REDIS_REPO}" | tail -n +2 | awk '{ print $1 ":" $2 }' | grep -v "${REDIS_IMAGE}" | xargs --no-run-if-empty docker rmi; then
echo "Removed old redis images"
fi
# only touch apps in installed state. any other state is just resumed by the taskmanager
if [[ "${infra_version}" == "none" ]]; then
# if no existing infra was found (for new, upgraded and restored cloudons), download app backups
echo "Marking installed apps for restore"
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore", oldConfigJson = NULL WHERE installationState = "installed"' box
else
# if existing infra was found, just mark apps for reconfiguration
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure", oldConfigJson = NULL WHERE installationState = "installed"' box
fi
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"
+179 -314
View File
@@ -7,7 +7,6 @@ exports = module.exports = {
restoreAddons: restoreAddons,
getEnvironment: getEnvironment,
getLinksSync: getLinksSync,
getBindsSync: getBindsSync,
getContainerNamesSync: getContainerNamesSync,
@@ -19,30 +18,34 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
ClientsError = clients.ClientsError,
debug = require('debug')('box:addons'),
docker = require('./docker.js').connection,
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
MemoryStream = require('memorystream'),
infra = require('./infra_version.js'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
uuid = require('node-uuid');
util = require('util');
var NOOP = function (app, options, callback) { return callback(); };
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = {
email: {
setup: setupEmail,
teardown: teardownEmail,
backup: NOOP,
restore: setupEmail
},
ldap: {
setup: setupLdap,
teardown: teardownLdap,
@@ -79,6 +82,12 @@ var KNOWN_ADDONS = {
backup: backupPostgreSql,
restore: restorePostgreSql
},
recvmail: {
setup: setupRecvMail,
teardown: teardownRecvMail,
backup: NOOP,
restore: setupRecvMail
},
redis: {
setup: setupRedis,
teardown: teardownRedis,
@@ -199,28 +208,6 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, callback);
}
function getLinksSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
var links = [ ];
if (!addons) return links;
for (var addon in addons) {
switch (addon) {
case 'mysql': links.push('mysql:mysql'); break;
case 'postgresql': links.push('postgresql:postgresql'); break;
case 'sendmail': links.push('mail:mail'); break;
case 'redis': links.push('redis-' + app.id + ':redis-' + app.id); break;
case 'mongodb': links.push('mongodb:mongodb'); break;
default: break;
}
}
return links;
}
function getBindsSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
@@ -267,22 +254,18 @@ function setupOauth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
debugApp(app, 'setupOauth: id:%s clientSecret:%s', id, clientSecret);
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(appId, clientdb.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_OAUTH, clientSecret, redirectURI, scope, function (error) {
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
var env = [
'OAUTH_CLIENT_ID=' + id,
'OAUTH_CLIENT_SECRET=' + clientSecret,
'OAUTH_CLIENT_ID=' + result.id,
'OAUTH_CLIENT_SECRET=' + result.clientSecret,
'OAUTH_ORIGIN=' + config.adminOrigin()
];
@@ -300,8 +283,8 @@ function teardownOauth(app, options, callback) {
debugApp(app, 'teardownOauth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_OAUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
@@ -313,23 +296,20 @@ function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var appId = app.id;
var id = 'cid-' + uuid.v4();
var scope = 'profile';
debugApp(app, 'setupSimpleAuth: id:%s', id);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
clientdb.add(id, appId, clientdb.TYPE_SIMPLE_AUTH, '', '', scope, function (error) {
clients.add(appId, clients.TYPE_SIMPLE_AUTH, '', scope, function (error, result) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.17.0.1',
'SIMPLE_AUTH_SERVER=172.18.0.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
'SIMPLE_AUTH_URL=http://172.18.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.18.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + result.id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
@@ -346,26 +326,57 @@ function teardownSimpleAuth(app, options, callback) {
debugApp(app, 'teardownSimpleAuth');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) console.error(error);
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2525',
'MAIL_IMAP_SERVER=mail',
'MAIL_IMAP_PORT=9993',
'MAIL_SIEVE_SERVER=mail',
'MAIL_SIEVE_PORT=4190',
'MAIL_DOMAIN=' + config.fqdn()
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
}
function teardownEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down Email');
appdb.unsetAddonConfig(app.id, 'email', callback);
}
function setupLdap(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var env = [
'LDAP_SERVER=172.17.0.1',
'LDAP_SERVER=172.18.0.1',
'LDAP_PORT=' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.0.1:' + config.get('ldapPort'),
'LDAP_URL=ldap://172.18.0.1:' + config.get('ldapPort'),
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
'LDAP_BIND_PASSWORD=' + hat(256) // this is ignored
'LDAP_BIND_PASSWORD=' + hat(4 * 128) // this is ignored
];
debugApp(app, 'Setting up LDAP');
@@ -388,18 +399,17 @@ function setupSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var username = app.location ? app.location + '-app' : 'no-reply'; // use no-reply for bare domains
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + username,
'MAIL_DOMAIN=' + config.fqdn()
];
var cmd = [ '/addons/mail/service.sh', 'add-send', from ];
debugApp(app, 'Setting up sendmail');
docker.execContainer('mail', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
}
function teardownSendMail(app, options, callback) {
@@ -407,9 +417,55 @@ function teardownSendMail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Tearing down sendmail');
var from = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
var cmd = [ '/addons/mail/service.sh', 'remove-send', from ];
debugApp(app, 'Tearing down sendmail : %j', cmd);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(error);
appdb.unsetAddonConfig(app.id, 'sendmail', callback);
});
}
function setupRecvMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Setting up recvmail');
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'add-recv', to ];
docker.execContainer('mail', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting recvmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
}
function teardownRecvMail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var to = (app.location ? app.location : app.manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
var cmd = [ '/addons/mail/service.sh', 'remove-recv', to ];
debugApp(app, 'Tearing down recvmail: %j', cmd);
docker.execContainer('mail', cmd, { }, function (error) {
if (error) return callback(error);
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
});
}
function setupMySql(app, options, callback) {
@@ -419,31 +475,14 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'add', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
}
@@ -452,24 +491,14 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mysql');
var cmd = [ '/addons/mysql/service.sh', 'remove', app.id ];
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
debugApp(app, 'Tearing down mysql');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mysql', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'mysql', callback);
});
});
appdb.unsetAddonConfig(app.id, 'mysql', callback);
});
}
@@ -481,15 +510,9 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mysql', '/addons/mysql/service.sh', 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMySql: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupMySql failed with status ' + code : null);
});
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('mysql', cmd, { stdout: output }, callback);
}
function restoreMySql(app, options, callback) {
@@ -503,17 +526,8 @@ function restoreMySql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mysql', '/addons/mysql/service.sh', 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMySql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restoreMySql failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
docker.execContainer('mysql', cmd, { stdin: input }, callback);
});
}
@@ -524,31 +538,14 @@ function setupPostgreSql(app, options, callback) {
debugApp(app, 'Setting up postgresql');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
}
@@ -557,24 +554,14 @@ function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
debugApp(app, 'Tearing down postgresql');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('postgresql', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
});
});
appdb.unsetAddonConfig(app.id, 'postgresql', callback);
});
}
@@ -586,19 +573,13 @@ function backupPostgreSql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'postgresql', '/addons/postgresql/service.sh', 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupPostgreSql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'backupPostgreSql failed with status ' + code : null);
});
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
}
function restorePostgreSql(app, options, callback) {
callback = once(callback); // ChildProcess exit may or may not be called after error
callback = once(callback);
setupPostgreSql(app, options, function (error) {
if (error) return callback(error);
@@ -608,17 +589,9 @@ function restorePostgreSql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'postgresql', '/addons/postgresql/service.sh', 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restorePostgreSql: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restorePostgreSql failed with status ' + code : null);
});
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
});
}
@@ -629,31 +602,14 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var stdout = new MemoryStream();
var stderr = new MemoryStream();
execContainer.modem.demuxStream(stream, stdout, stderr);
stderr.on('data', function (data) { debugApp(app, data.toString('utf8')); }); // set -e output
var chunks = [ ];
stdout.on('data', function (chunk) { chunks.push(chunk); });
stream.on('error', callback);
stream.on('end', function () {
var env = Buffer.concat(chunks).toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
}
@@ -662,24 +618,14 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
debugApp(app, 'Tearing down mongodb');
container.exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true }, function (error, execContainer) {
docker.execContainer('mongodb', cmd, { }, function (error) {
if (error) return callback(error);
execContainer.start(function (error, stream) {
if (error) return callback(error);
var data = '';
stream.on('error', callback);
stream.on('data', function (d) { data += d.toString('utf8'); });
stream.on('end', function () {
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
});
appdb.unsetAddonConfig(app.id, 'mongodb', callback);
});
}
@@ -691,15 +637,9 @@ function backupMongoDb(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
output.on('error', callback);
var cp = spawn('/usr/bin/docker', [ 'exec', 'mongodb', '/addons/mongodb/service.sh', 'backup', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupMongoDb: done %s %s', code, signal);
if (!callback.called) callback(code ? 'backupMongoDb failed with status ' + code : null);
});
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
cp.stdout.pipe(output);
cp.stderr.pipe(process.stderr);
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
}
function restoreMongoDb(app, options, callback) {
@@ -713,53 +653,11 @@ function restoreMongoDb(app, options, callback) {
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
input.on('error', callback);
// cannot get this to work through docker.exec
var cp = spawn('/usr/bin/docker', [ 'exec', '-i', 'mongodb', '/addons/mongodb/service.sh', 'restore', app.id ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'restoreMongoDb: done %s %s', code, signal);
if (!callback.called) callback(code ? 'restoreMongoDb failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
input.pipe(cp.stdin).on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
});
}
function forwardRedisPort(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainer('redis-' + appId).inspect(function (error, data) {
if (error) return callback(new Error('Unable to inspect container:' + error));
var redisPort = parseInt(safe.query(data, 'NetworkSettings.Ports.6379/tcp[0].HostPort'), 10);
if (!Number.isInteger(redisPort)) return callback(new Error('Unable to get container port mapping'));
return callback(null);
});
}
function stopAndRemoveRedis(container, callback) {
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) debug('stopAndRemoveRedis: Ignored error:', error);
callback();
});
};
}
// stopping redis with SIGTERM makes it commit the database to disk
async.series([
ignoreError(container.stop.bind(container, { t: 10 })),
ignoreError(container.wait.bind(container)),
ignoreError(container.remove.bind(container, { force: true, v: true }))
], callback);
}
// Ensures that app's addon redis container is running. Can be called when named container already exists/running
function setupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
@@ -776,57 +674,32 @@ function setupRedis(app, options, callback) {
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
var createOptions = {
name: 'redis-' + app.id,
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null,
Volumes: {
'/tmp': {},
'/run': {}
},
VolumesFrom: [],
HostConfig: {
Binds: [
redisVarsFile + ':/etc/redis/redis_vars.sh:ro',
redisDataDir + ':/var/lib/redis:rw'
],
Memory: 1024 * 1024 * 75, // 100mb
MemorySwap: 1024 * 1024 * 75 * 2, // 150mb
PortBindings: {
'6379/tcp': [{ HostPort: '0', HostIp: '127.0.0.1' }]
},
ReadonlyRootfs: true,
RestartPolicy: {
'Name': 'always',
'MaximumRetryCount': 0
}
}
};
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const cmd = `docker run --restart=always -d --name=${redisName} \
--net cloudron \
--net-alias ${redisName} \
-m 100m \
--memory-swap 150m \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
var env = [
'REDIS_URL=redis://redisuser:' + redisPassword + '@redis-' + app.id,
'REDIS_PASSWORD=' + redisPassword,
'REDIS_HOST=redis-' + app.id,
'REDIS_HOST=' + redisName,
'REDIS_PORT=6379'
];
var redisContainer = docker.getContainer(createOptions.name);
stopAndRemoveRedis(redisContainer, function () {
docker.createContainer(createOptions, function (error) {
if (error && error.statusCode !== 409) return callback(error); // if not already created
redisContainer.start(function (error) {
if (error && error.statusCode !== 304) return callback(error); // if not already running
appdb.setAddonConfig(app.id, 'redis', env, function (error) {
if (error) return callback(error);
forwardRedisPort(app.id, callback);
});
});
});
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
}
@@ -835,7 +708,7 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = docker.getContainer('redis-' + app.id);
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
@@ -858,15 +731,7 @@ function teardownRedis(app, options, callback) {
function backupRedis(app, options, callback) {
debugApp(app, 'Backing up redis');
callback = once(callback); // ChildProcess exit may or may not be called after error
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
var cp = spawn('/usr/bin/docker', [ 'exec', 'redis-' + app.id, '/addons/redis/service.sh', 'backup' ]);
cp.on('error', callback);
cp.on('exit', function (code, signal) {
debugApp(app, 'backupRedis: done. code:%s signal:%s', code, signal);
if (!callback.called) callback(code ? 'backupRedis failed with status ' + code : null);
});
cp.stdout.pipe(process.stdout);
cp.stderr.pipe(process.stderr);
docker.execContainer('redis-' + app.id, cmd, { }, callback);
}
+26 -34
View File
@@ -4,7 +4,6 @@
exports = module.exports = {
get: get,
getBySubdomain: getBySubdomain,
getByHttpPort: getByHttpPort,
getByContainerId: getByContainerId,
add: add,
@@ -27,6 +26,7 @@ exports = module.exports = {
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
@@ -59,7 +59,8 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson', 'apps.oauthProxy' ].join(',');
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
'apps.xFrameOptions', 'apps.oauthProxy' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -70,10 +71,6 @@ function postProcess(result) {
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.lastBackupConfigJson === null || typeof result.lastBackupConfigJson === 'string');
result.lastBackupConfig = safe.JSON.parse(result.lastBackupConfigJson);
delete result.lastBackupConfigJson;
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
@@ -92,12 +89,15 @@ function postProcess(result) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
}
result.oauthProxy = !!result.oauthProxy;
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
// TODO remove later once all apps have this attribute
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
result.oauthProxy = !!result.oauthProxy; // make it bool
}
function get(id, callback) {
@@ -116,22 +116,6 @@ function get(id, callback) {
});
}
function getBySubdomain(subdomain, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof httpPort, 'number');
assert.strictEqual(typeof callback, 'function');
@@ -179,26 +163,33 @@ function getAll(callback) {
});
}
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, oauthProxy, callback) {
function add(id, appStoreId, manifest, location, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof accessRestriction, 'object');
assert.strictEqual(typeof oauthProxy, 'boolean');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
var manifestJson = JSON.stringify(manifest);
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var lastBackupId = data.lastBackupId || null; // used when cloning
var oauthProxy = data.oauthProxy || false;
var queries = [ ];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, oauthProxy ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, oauthProxy) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, oauthProxy ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -283,6 +274,7 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof constraints, 'string');
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
var queries = [ ];
@@ -301,9 +293,6 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest') {
fields.push('manifestJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'lastBackupConfig') {
fields.push('lastBackupConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
@@ -362,14 +351,17 @@ function setInstallationCommand(appId, installationState, values, callback) {
// 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
// update and configure are allowed only in installed state
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
} else {
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
}
+20 -8
View File
@@ -3,6 +3,7 @@
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
@@ -16,7 +17,7 @@ exports = module.exports = {
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 3 * 60 * 1000; // 3 minutes
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
@@ -24,8 +25,11 @@ var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
}
function setHealth(app, health, callback) {
@@ -89,6 +93,7 @@ function checkAppHealth(app, callback) {
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
.get(healthCheckUrl)
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
@@ -114,7 +119,7 @@ function processApps(callback) {
var alive = apps
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return a.location; }).join(', ');
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
@@ -133,13 +138,16 @@ function run() {
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
docker run -ti -m 100M cloudron/base:0.9.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
@@ -149,15 +157,19 @@ function processDockerEvents() {
stream.on('data', function (data) {
var ev = JSON.parse(data);
debug('Container ' + ev.id + ' went OOM');
appdb.getByContainerId(ev.id, function (error, app) {
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var now = new Date();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
debug('OOM Context: %s', context);
// do not send mails for dev apps
if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
if ((!app || app.appStoreId !== '') && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
lastOomMailTime = now;
}
});
});
@@ -189,7 +201,7 @@ function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
gDockerEventStream.end();
if (gDockerEventStream) gDockerEventStream.end();
callback();
}
+582 -378
View File
File diff suppressed because it is too large Load Diff
+92 -52
View File
@@ -1,7 +1,5 @@
#!/usr/bin/env node
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -19,7 +17,8 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
};
require('supererror')({ splatchError: true });
@@ -35,16 +34,16 @@ var addons = require('./addons.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
ejs = require('ejs'),
fs = require('fs'),
hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
@@ -57,7 +56,7 @@ var addons = require('./addons.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
uuid = require('node-uuid'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
@@ -100,9 +99,7 @@ function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(app.location);
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
@@ -164,20 +161,20 @@ function allocateOAuthProxyCredentials(app, callback) {
if (!app.oauthProxy) return callback(null);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
debugApp(app, 'Creating oauth proxy credentials');
var redirectURI = 'https://' + config.appFqdn(app.location);
var scope = 'profile';
clientdb.add(id, app.id, clientdb.TYPE_PROXY, clientSecret, redirectURI, scope, callback);
clients.add(app.id, clients.TYPE_PROXY, redirectURI, scope, callback);
}
function removeOAuthProxyCredentials(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
clientdb.delByAppIdAndType(app.id, clientdb.TYPE_PROXY, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) {
clients.delByAppIdAndType(app.id, clients.TYPE_PROXY, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) {
debugApp(app, 'Error removing OAuth client id', error);
return callback(error);
}
@@ -231,17 +228,20 @@ function downloadIcon(app, callback) {
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
superagent
.get(iconUrl)
.buffer(true)
.end(function (error, res) {
if (error && !error.response) return callback(new Error('Network error downloading icon:' + error.message));
if (res.statusCode !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
.get(iconUrl)
.buffer(true)
.timeout(30 * 1000)
.end(function (error, res) {
if (error && !error.response) return retryCallback(new Error('Network error downloading icon:' + error.message));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
callback(null);
});
retryCallback(null);
});
}, callback);
}
function registerSubdomain(app, callback) {
@@ -256,10 +256,19 @@ function registerSubdomain(app, callback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
subdomains.add(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
// get the current record before updating it
subdomains.get(app.location, 'A', function (error, values) {
if (error) return retryCallback(error);
retryCallback(null, error || changeId);
// refuse to update any existing DNS record for custom domains that we did not create
// note that the appstore sets up the naked domain for non-custom domains
if (config.isCustomDomain() && values.length !== 0 && !app.dnsRecordId) return retryCallback(null, new Error('DNS Record already exists'));
subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
@@ -275,7 +284,7 @@ function unregisterSubdomain(app, location, callback) {
assert.strictEqual(typeof callback, 'function');
// do not unregister bare domain because we show a error/cloudron info page there
if (location === '') {
if (!config.isCustomDomain() && location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
@@ -318,20 +327,25 @@ function waitForDnsPropagation(app, callback) {
return callback(null);
}
function retry(error) {
debugApp(app, 'waitForDnsPropagation: ', error);
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
}
async.retry({ interval: 5000, times: 120 }, function checkStatus(retryCallback) {
subdomains.status(app.dnsRecordId, function (error, result) {
if (error) return retryCallback(new Error('Failed to get dns record status : ' + error.message));
subdomains.status(app.dnsRecordId, function (error, result) {
if (error) return retry(new Error('Failed to get dns record status : ' + error.message));
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
if (result !== 'done') return retryCallback(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
retryCallback(null, result);
});
}, callback);
}
callback(null);
});
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
}
// updates the app object and the database
@@ -410,9 +424,12 @@ function install(app, callback) {
runApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
@@ -436,7 +453,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
apps.backupApp.bind(null, app, app.manifest.addons),
backups.backupApp.bind(null, app, app.manifest),
// done!
function (callback) {
@@ -501,7 +518,7 @@ function restore(app, callback) {
createVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }),
apps.restoreApp.bind(null, app, app.manifest.addons, backupId),
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
updateApp.bind(null, app, { installationProgress: '75, Creating container' }),
createContainer.bind(null, app),
@@ -511,9 +528,12 @@ function restore(app, callback) {
runApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
configureNginx.bind(null, app),
@@ -573,6 +593,9 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain CNAME setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
@@ -598,19 +621,24 @@ function update(app, callback) {
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
async.series([
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
verifyManifest.bind(null, app),
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
addons.teardownAddons.bind(null, app, unusedAddons),
function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
@@ -622,16 +650,16 @@ function update(app, callback) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '20, Backup app' }),
apps.backupApp.bind(null, app, app.oldConfig.manifest.addons)
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest)
], next);
},
updateApp.bind(null, app, { installationProgress: '35, Downloading icon' }),
downloadIcon.bind(null, app),
// only delete unused addons after backup
addons.teardownAddons.bind(null, app, unusedAddons),
updateApp.bind(null, app, { installationProgress: '45, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -697,7 +725,13 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
], callback);
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
}
callback(null);
});
}
function runApp(app, callback) {
@@ -758,9 +792,10 @@ function startTask(appId, callback) {
case appdb.ISTATE_PENDING_BACKUP: return backup(app, callback);
case appdb.ISTATE_INSTALLED: return handleRunCommand(app, callback);
case appdb.ISTATE_PENDING_INSTALL: return install(app, callback);
case appdb.ISTATE_PENDING_CLONE: return restore(app, callback);
case appdb.ISTATE_PENDING_FORCE_UPDATE: return update(app, callback);
case appdb.ISTATE_ERROR:
debugApp(app, 'Apptask launched with error states.');
debugApp(app, 'Internal error. apptask launched with error status.');
return callback(null);
default:
debugApp(app, 'apptask launched with invalid command');
@@ -774,6 +809,11 @@ if (require.main === module) {
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
debug('taskmanager sent SIGTERM since it got a new task for this app');
process.exit(0);
});
initialize(function (error) {
if (error) throw error;
+12 -33
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
@@ -10,8 +8,9 @@ exports = module.exports = {
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clientdb = require('./clientdb'),
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
@@ -19,7 +18,6 @@ var assert = require('assert'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
userdb = require('./userdb'),
UserError = user.UserError,
_ = require('underscore');
@@ -27,11 +25,11 @@ function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.username);
callback(null, user.id);
});
passport.deserializeUser(function(username, callback) {
userdb.get(username, function (error, result) {
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
@@ -43,7 +41,7 @@ function initialize(callback) {
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verify(username, password, function (error, result) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
@@ -66,14 +64,14 @@ function initialize(callback) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clientdb.get(username, function (error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
user.verify(username, password, function (error, result) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
@@ -84,8 +82,8 @@ function initialize(callback) {
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clientdb.get(clientId, function(error, client) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
@@ -100,29 +98,11 @@ function initialize(callback) {
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
var tokenType;
if (token.identifier.indexOf(tokendb.PREFIX_DEV) === 0) {
token.identifier = token.identifier.slice(tokendb.PREFIX_DEV.length);
tokenType = tokendb.TYPE_DEV;
} else if (token.identifier.indexOf(tokendb.PREFIX_APP) === 0) {
tokenType = tokendb.TYPE_APP;
return callback(null, { id: token.identifier.slice(tokendb.PREFIX_APP.length), tokenType: tokenType }, info);
} else if (token.identifier.indexOf(tokendb.PREFIX_USER) === 0) {
tokenType = tokendb.TYPE_USER;
token.identifier = token.identifier.slice(tokendb.PREFIX_USER.length);
} else {
// legacy tokens assuming a user access token
tokenType = tokendb.TYPE_USER;
}
userdb.get(token.identifier, function (error, user) {
user.get(token.identifier, function (error, user) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// amend the tokenType of the token owner
user.tokenType = tokenType;
callback(null, user, info);
});
});
@@ -136,4 +116,3 @@ function uninitialize(callback) {
callback(null);
}
+114
View File
@@ -0,0 +1,114 @@
'use strict';
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', ];
exports = module.exports = {
add: add,
getPaged: getPaged,
get: get,
del: del,
getByAppIdPaged: getByAppIdPaged,
_clear: clear,
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
};
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
}
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_BOX, exports.BACKUP_STATE_NORMAL, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, 'appbackup\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
[ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function add(backup, callback) {
assert(backup && typeof backup === 'object');
assert.strictEqual(typeof backup.id, 'string');
assert.strictEqual(typeof backup.version, 'string');
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
assert(util.isArray(backup.dependsOn));
assert.strictEqual(typeof callback, 'function');
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn) VALUES (?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(',') ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE backups', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+391 -36
View File
@@ -3,21 +3,56 @@
exports = module.exports = {
BackupsError: BackupsError,
getAllPaged: getAllPaged,
getPaged: getPaged,
getByAppIdPaged: getByAppIdPaged,
getBackupUrl: getBackupUrl,
getRestoreUrl: getRestoreUrl,
getRestoreConfig: getRestoreConfig,
copyLastBackup: copyLastBackup
ensureBackup: ensureBackup,
backup: backup,
backupApp: backupApp,
restoreApp: restoreApp,
backupBoxAndApps: backupBoxAndApps
};
var assert = require('assert'),
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
caas = require('./storage/caas.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
locker = require('./locker.js'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
s3 = require('./storage/s3.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
util = require('util');
superagent = require('superagent'),
util = require('util'),
webhooks = require('./webhooks.js');
var BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -40,6 +75,7 @@ function BackupsError(reason, errorOrMessage) {
util.inherits(BackupsError, Error);
BackupsError.EXTERNAL_ERROR = 'external error';
BackupsError.INTERNAL_ERROR = 'internal error';
BackupsError.BAD_STATE = 'bad state';
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
// choose which storage backend we use for test purpose we use s3
@@ -51,49 +87,105 @@ function api(provider) {
}
}
function getAllPaged(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
function getPaged(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
backupdb.getPaged(page, perPage, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
backupdb.getByAppIdPaged(page, perPage, appId, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getBoxBackupCredentials(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
var now = new Date();
var filebase = util.format('backup_%s-v%s', now.toISOString(), config.version());
var filename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
if (error) return callback(error);
return callback(null, backups); // [ { creationTime, restoreKey } ] sorted by time (latest first
result.id = filename;
result.s3Url = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + filename;
result.backupKey = backupConfig.key;
debug('getBoxBackupCredentials: %j', result);
callback(null, result);
});
});
}
function getBackupUrl(app, callback) {
assert(!app || typeof app === 'object');
function getAppBackupCredentials(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
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 now = new Date();
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), manifest.version);
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedUploadUrl(backupConfig, filename, function (error, result) {
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
if (error) return callback(error);
var obj = {
id: filename,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
result.id = dataFilename;
result.s3ConfigUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + configFilename;
result.s3DataUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + dataFilename;
result.backupKey = backupConfig.key;
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
debug('getAppBackupCredentials: %j', result);
callback(null, obj);
callback(null, result);
});
});
}
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
function getRestoreConfig(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
var configFile = backupId.replace(/\.tar\.gz$/, '.json');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getRestoreUrl(backupConfig, configFile, function (error, result) {
if (error) return callback(error);
superagent.get(result.url).buffer(true).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (response.statusCode !== 200) return callback(new Error('Invalid response code when getting config.json : ' + response.statusCode));
var config = safe.JSON.parse(response.text);
if (!config) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error in config:' + safe.error.message));
return callback(null, config);
});
});
});
}
@@ -106,37 +198,300 @@ function getRestoreUrl(backupId, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).getSignedDownloadUrl(backupConfig, backupId, function (error, result) {
api(backupConfig.provider).getRestoreUrl(backupConfig, backupId, function (error, result) {
if (error) return callback(error);
var obj = {
id: backupId,
url: result.url,
sessionToken: result.sessionToken,
backupKey: backupConfig.key
};
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
debug('getRestoreUrl: id:%s url:%s backupKey:%s', obj.id, obj.url, obj.backupKey);
callback(null, obj);
});
});
}
function copyLastBackup(app, callback) {
assert(app && typeof app === 'object');
function copyLastBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var toFilename = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
var now = new Date();
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), manifest.version);
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), manifest.version);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilename, function (error) {
debug('copyLastBackup: copying archive %s to %s', app.lastBackupId, toFilenameArchive);
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilename);
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
debug('copyLastBackup: copying config %s to %s', configFileId, toFilenameConfig);
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, toFilenameArchive);
});
});
});
}
function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
getBoxBackupCredentials(appBackupIds, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('backupBoxWithAppBackupIds: %j', result);
var args = [ result.s3Url, result.accessKeyId, result.secretAccessKey, result.region, result.backupKey ];
if (result.sessionToken) args.push(result.sessionToken);
shell.sudo('backupBox', [ BACKUP_BOX_CMD ].concat(args), function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('backupBoxWithAppBackupIds: success');
backupdb.add({ id: result.id, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
});
});
});
}
// this function expects you to have a lock
// function backupBox(callback) {
// apps.getAll(function (error, allApps) {
// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
//
// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
//
// backupBoxWithAppBackupIds(appBackupIds, callback);
// });
// }
function canBackupApp(app) {
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
// 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) ||
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app.lastBackupId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
copyLastBackup(app, manifest, function (error, newBackupId) {
if (error) return callback(error);
debugApp(app, 'reuseOldAppBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
callback(null, newBackupId);
});
}
function createNewAppBackup(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
getAppBackupCredentials(app, manifest, function (error, result) {
if (error) return callback(error);
debugApp(app, 'createNewAppBackup: backup url:%s backup config url:%s', result.s3DataUrl, result.s3ConfigUrl);
var args = [ app.id, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey, result.region, result.backupKey ];
if (result.sessionToken) args.push(result.sessionToken);
async.series([
addons.backupAddons.bind(null, app, manifest.addons),
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD ].concat(args))
], function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debugApp(app, 'createNewAppBackup: %s done', result.id);
backupdb.add({ id: result.id, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, result.id);
});
});
});
}
function setRestorePoint(appId, lastBackupId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
return callback(null);
});
}
function backupApp(app, manifest, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var backupFunction;
if (!canBackupApp(app)) {
if (!app.lastBackupId) {
debugApp(app, 'backupApp: cannot backup app');
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
}
backupFunction = reuseOldAppBackup.bind(null, app, manifest);
} else {
var appConfig = apps.getAppConfig(app);
appConfig.manifest = manifest;
backupFunction = createNewAppBackup.bind(null, app, manifest);
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
return callback(safe.error);
}
}
backupFunction(function (error, backupId) {
if (error) return callback(error);
debugApp(app, 'backupApp: successful id:%s', backupId);
setRestorePoint(app.id, backupId, function (error) {
if (error) return callback(error);
return callback(null, backupId);
});
});
}
// this function expects you to have a lock
function backupBoxAndApps(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
callback = callback || NOOP_CALLBACK;
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
apps.getAll(function (error, allApps) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
var processed = 0;
var step = 100/(allApps.length+1);
progress.set(progress.BACKUP, processed, '');
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
backupApp(app, app.manifest, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
callback(error, filename);
});
});
});
}
function backup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
if (error) debug('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
});
callback(null);
}
function ensureBackup(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
getPaged(1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(auditSource, callback);
});
}
function restoreApp(app, addonsToRestore, backupId, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
assert(app.lastBackupId);
getRestoreUrl(backupId, function (error, result) {
if (error) return callback(error);
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
addons.restoreAddons(app, addonsToRestore, callback);
});
});
}
+64 -37
View File
@@ -1,5 +1,3 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
@@ -7,6 +5,7 @@ var assert = require('assert'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
@@ -17,10 +16,13 @@ var assert = require('assert'),
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf';
exports = module.exports = {
getCertificate: getCertificate
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function AcmeError(reason, errorOrMessage) {
@@ -58,12 +60,11 @@ function Acme(options) {
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.chainPem = options.prod ? safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt') : new Buffer('');
}
Acme.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory', function (error, response) {
if (error) return callback(error);
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
@@ -117,7 +118,7 @@ Acme.prototype.sendSignedRequest = function (url, payload, callback) {
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).end(function (error, res) {
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
@@ -258,7 +259,7 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).end(function (error, result) {
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
@@ -304,7 +305,7 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
@@ -315,25 +316,57 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
var execSync = safe.child_process.execSync;
var privateKeyFile = path.join(outdir, domain + '.key');
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var csrFile = path.join(outdir, domain + '.csr');
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
superagent.get(this.caOrigin + linkInfo.up).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var execSync = safe.child_process.execSync;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
@@ -346,7 +379,7 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).end(function (error, result) {
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
@@ -355,18 +388,22 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
var execSync = safe.child_process.execSync;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file saved');
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, that.chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
debug('downloadCertificate: cert file saved at %s', certificateFile);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback();
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
@@ -414,21 +451,11 @@ Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
var certificateGetter;
if (certUrl) {
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
certificateGetter = this.downloadCertificate.bind(this, domain, certUrl);
} else {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
certificateGetter = this.acmeFlow.bind(this, domain);
}
certificateGetter(function (error) {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
+4 -1
View File
@@ -1,7 +1,10 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate
getCertificate: getCertificate,
// testing
_name: 'caas'
};
var assert = require('assert'),
@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
-----END CERTIFICATE-----
+157 -60
View File
@@ -1,16 +1,30 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
renewAll: renewAll,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
getAdminCertificatePath: getAdminCertificatePath,
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
@@ -22,18 +36,6 @@ var acme = require('./cert/acme.js'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -55,22 +57,31 @@ function CertificatesError(reason, errorOrMessage) {
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
function getApi(callback) {
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
var api = tlsConfig.provider === 'caas' ? caas : acme;
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider) !== 'caas' ? acme : caas;
var options = { };
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
if (tlsConfig.provider === 'caas') {
options.prod = !config.isDev(); // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
@@ -78,8 +89,6 @@ function getApi(callback) {
}
function installAdminCertificate(callback) {
if (cloudron.isConfiguredSync()) return callback();
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
@@ -88,10 +97,10 @@ function installAdminCertificate(callback) {
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
waitForDns(config.adminFqdn(), ip, config.fqdn(), function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return callback(error);
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
@@ -104,41 +113,100 @@ function installAdminCertificate(callback) {
});
}
function needsRenewalSync(certFilePath) {
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
return result === null; // command errored
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
if (!filenames) {
debug('autoRenew: Error getting filenames: %s', safe.error.message);
return;
}
debug('renewAll: Checking certificates for renewal');
var certs = filenames.filter(function (f) {
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
});
debug('autoRenew: %j needs to be renewed', certs);
getApi(function (error, api, apiOptions) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
var domain = cert.match(/^(.*)\.cert$/)[1];
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location);
api.getCertificate(domain, apiOptions, function (error) {
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
iteratorCallback(); // move on to next cert
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
debug('renewAll: existing user key file for %s. skipping', appDomain);
continue;
}
// check if we have an auto cert to be renewed
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || config.appFqdn(app.location);
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
if (error) {
debug('renewAll: could not renew cert for %s because %s', domain, error);
mailer.certificateRenewalError(domain, errorMessage);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
certFilePath = 'cert/host.cert';
keyFilePath = 'cert/host.key';
} else {
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.location === constants.ADMIN_LOCATION ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
iteratorCallback(); // move to next app
});
});
});
});
});
@@ -207,12 +275,20 @@ function setFallbackCertificate(cert, key, callback) {
});
}
function getFallbackCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
@@ -226,23 +302,44 @@ function setAdminCertificate(cert, key, callback) {
nginx.configureAdmin(certFilePath, keyFilePath, callback);
}
function ensureCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
getFallbackCertificatePath(callback);
}
debug('ensureCertificate: %s cert require renewal', domain);
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var domain = app.altDomain || config.appFqdn(app.location);
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
return callback(null, certFilePath, keyFilePath);
}
getApi(function (error, api, apiOptions) {
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
}
debug('ensureCertificate: %s cert require renewal', domain);
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
+19 -14
View File
@@ -5,6 +5,7 @@
exports = module.exports = {
get: get,
getAll: getAll,
getAllWithTokenCount: getAllWithTokenCount,
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
add: add,
del: del,
@@ -14,13 +15,7 @@ exports = module.exports = {
delByAppId: delByAppId,
delByAppIdAndType: delByAppIdAndType,
_clear: clear,
TYPE_EXTERNAL: 'external',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy',
TYPE_ADMIN: 'admin'
_clear: clear
};
var assert = require('assert'),
@@ -52,14 +47,24 @@ function getAll(callback) {
});
}
function getAllWithTokenCount(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
});
}
function getAllWithTokenCountByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
callback(null, results);
});
}
@@ -71,7 +76,7 @@ function getByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -84,7 +89,7 @@ function getByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null, result[0]);
callback(null, result[0]);
});
}
@@ -127,7 +132,7 @@ function delByAppId(appId, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
@@ -140,17 +145,17 @@ function delByAppIdAndType(appId, type, callback) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId!="webadmin"', function (error) {
database.query('DELETE FROM clients WHERE id!="cid-webadmin" AND id!="cid-sdk" AND id!="cid-cli"', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null);
callback(null);
});
}
+153 -22
View File
@@ -6,9 +6,32 @@ exports = module.exports = {
add: add,
get: get,
del: del,
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId
delClientTokensByUserId: delClientTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
delToken: delToken,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer',
SCOPE_PROFILE: 'profile',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy'
};
var assert = require('assert'),
@@ -16,7 +39,6 @@ var assert = require('assert'),
hat = require('hat'),
appdb = require('./appdb.js'),
tokendb = require('./tokendb.js'),
constants = require('./constants.js'),
async = require('async'),
clientdb = require('./clientdb.js'),
DatabaseError = require('./databaseerror.js'),
@@ -43,14 +65,41 @@ function ClientsError(reason, errorOrMessage) {
util.inherits(ClientsError, Error);
ClientsError.INVALID_SCOPE = 'Invalid scope';
ClientsError.INVALID_CLIENT = 'Invalid client';
ClientsError.INVALID_TOKEN = 'Invalid token';
ClientsError.BAD_FIELD = 'Bad field';
ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
if (/[^a-zA-Z0-9\-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
return null;
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
if (scope === '*') return null;
var VALID_SCOPES = [
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
// TODO maybe validate all individual scopes if they exist
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
return null;
}
@@ -62,11 +111,18 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = validateScope(scope);
if (error) return callback(error);
// appId is also client name
error = validateName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(256);
var clientSecret = hat(8 * 128);
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
@@ -89,6 +145,7 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
@@ -99,24 +156,24 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.del(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getAllWithDetailsByUserId(userId, callback) {
assert.strictEqual(typeof userId, 'string');
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getAllWithTokenCountByIdentifier(tokendb.PREFIX_USER + userId, function (error, results) {
clientdb.getAll(function (error, results) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
var tmp = [];
async.each(results, function (record, callback) {
if (record.type === clientdb.TYPE_ADMIN) {
record.name = constants.ADMIN_NAME;
record.location = constants.ADMIN_LOCATION;
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
// the appId in this case holds the name
record.name = record.appId;
tmp.push(record);
@@ -125,14 +182,13 @@ function getAllWithDetailsByUserId(userId, callback) {
appdb.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', result, error);
console.error('Failed to get app details for oauth client', record.appId, error);
return callback(null); // ignore error so we continue listing clients
}
if (record.type === clientdb.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === clientdb.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === clientdb.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
if (record.type === clientdb.TYPE_EXTERNAL) record.name = result.manifest.title + ' external';
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
record.location = result.location;
@@ -147,15 +203,27 @@ function getAllWithDetailsByUserId(userId, callback) {
});
}
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.getByAppIdAndType(appId, type, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null, result);
});
}
function getClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error, result) {
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null, []);
});
@@ -171,10 +239,10 @@ function delClientTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error) {
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
clientdb.get(clientId, function (error/*, result*/) {
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null);
});
@@ -184,3 +252,66 @@ function delClientTokensByUserId(clientId, userId, callback) {
callback(null);
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getByAppIdAndType(appId, type, function (error, result) {
if (error) return callback(error);
tokendb.delByClientId(result.id, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
clientdb.delByAppIdAndType(appId, type, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
if (error) return callback(error);
callback(null);
});
});
});
}
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
});
});
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error, result) {
if (error) return callback(error);
tokendb.del(tokenId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null);
});
});
}
+193 -237
View File
@@ -1,5 +1,3 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
@@ -14,12 +12,9 @@ exports = module.exports = {
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
update: update,
reboot: reboot,
migrate: migrate,
backup: backup,
retire: retire,
ensureBackup: ensureBackup,
migrate: migrate,
isConfiguredSync: isConfiguredSync,
@@ -32,15 +27,16 @@ exports = module.exports = {
};
var apps = require('./apps.js'),
AppsError = require('./apps.js').AppsError,
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
clientdb = require('./clientdb.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:cloudron'),
df = require('node-df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
@@ -50,6 +46,7 @@ var apps = require('./apps.js'),
progress = require('./progress.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
@@ -58,38 +55,33 @@ var apps = require('./apps.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
userdb = require('./userdb.js'),
user = require('./user.js'),
util = require('util'),
webhooks = require('./webhooks.js');
_ = require('underscore');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
var gUpdatingDns = false, // flag for dns update reentrancy
gCloudronDetails = null, // cached cloudron details like region,size...
gBoxAndUserDetails = null, // cached cloudron details like region,size...
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
function debugApp(app, args) {
assert(!app || typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
callback();
});
};
}
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -113,19 +105,24 @@ CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.BAD_USERNAME = 'Bad username';
CloudronError.BAD_EMAIL = 'Bad email';
CloudronError.BAD_PASSWORD = 'Bad password';
CloudronError.BAD_NAME = 'Bad name';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
ensureDkimKeySync();
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
if (!fs.existsSync(paths.FIRST_RUN_FILE)) {
debug('initialize: installing app bundle on first run');
process.nextTick(installAppBundle);
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
}
syncConfigState(callback);
}
@@ -133,6 +130,7 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
exports.events.removeListener(exports.EVENT_FIRST_RUN, installAppBundle);
callback(null);
}
@@ -185,13 +183,19 @@ function setTimeZone(ip, callback) {
debug('setTimeZone ip:%s', ip);
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
// https://github.com/bluesmoon/node-geoip
// https://github.com/runk/node-maxmind
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
superagent.get('http://ip-api.com/json/' + ip).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
if (!result.body.timezone) {
if (!result.body.timezone || typeof result.body.timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
@@ -202,38 +206,39 @@ function setTimeZone(ip, callback) {
});
}
function activate(username, password, email, displayName, ip, callback) {
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, function (error, userObject) {
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clientdb.getByAppIdAndType('webadmin', clientdb.TYPE_ADMIN, function (error, result) {
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// EE API is sync. do not keep the REST API reponse waiting
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
callback(null, { token: token, expires: expires });
});
});
@@ -243,7 +248,7 @@ function activate(username, password, email, displayName, ip, callback) {
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
userdb.count(function (error, count) {
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
@@ -261,46 +266,35 @@ function getStatus(callback) {
});
}
function getCloudronDetails(callback) {
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gCloudronDetails) return callback(null, gCloudronDetails);
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
if (!config.token()) {
gCloudronDetails = {
region: null,
size: null
};
return callback(null, gCloudronDetails);
}
// only supported for caas
if (config.provider() !== 'caas') return callback(null, {});
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gCloudronDetails = result.body.box;
gBoxAndUserDetails = result.body;
return callback(null, gCloudronDetails);
return callback(null, gBoxAndUserDetails);
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getCloudronDetails(function (error, result) {
if (error) {
debug('Failed to fetch cloudron details.', error);
getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
// set fallback values to avoid dependency on appstore
result = {
region: result ? result.region : null,
size: result ? result.size : null
};
}
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
@@ -321,9 +315,13 @@ function getConfig(callback) {
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
developerMode: developerMode,
region: result.region,
size: result.size,
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
@@ -338,13 +336,28 @@ function sendHeartbeat() {
if (!config.token()) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(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(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
}
function ensureDkimKeySync() {
var dkimPrivateKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/private');
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
if (fs.existsSync(dkimPrivateKeyFile) && fs.existsSync(dkimPublicKeyFile)) {
debug('DKIM keys already present');
return;
}
debug('Generating new DKIM keys');
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
}
function readDkimPublicKeySync() {
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
@@ -360,6 +373,7 @@ function readDkimPublicKeySync() {
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -373,16 +387,16 @@ function txtRecordsWithSpf(callback) {
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
validSpf = txtRecords[i].indexOf(' a:' + config.fqdn() + ' ') !== -1;
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
break;
}
if (validSpf) return callback(null, null);
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ~all"';
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.fqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
}
return callback(null, txtRecords);
@@ -404,13 +418,12 @@ function addDnsRecords() {
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
// DMARC requires special setup if report email id is in different domain
@@ -421,6 +434,9 @@ function addDnsRecords() {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for non-custom domains, we show a nakeddomain.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
@@ -438,7 +454,7 @@ function addDnsRecords() {
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
@@ -460,51 +476,9 @@ function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function migrate(size, region, callback) {
assert.strictEqual(typeof size, 'string');
assert.strictEqual(typeof region, 'string');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
if (error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
} else {
debug('Migration initiated successfully');
// do not unlock; cloudron is migrating
}
return;
}
// initiate the migration in the background
backupBoxAndApps(function (error, restoreKey) {
if (error) return unlock(error);
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send({ size: size, region: region, restoreKey: restoreKey })
.end(function (error, result) {
if (error && !error.response) return unlock(error);
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return unlock(null);
});
});
callback(null);
}
function update(boxUpdateInfo, callback) {
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
@@ -512,6 +486,8 @@ function update(boxUpdateInfo, callback) {
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
@@ -543,13 +519,16 @@ function update(boxUpdateInfo, callback) {
}
function updateToLatest(callback) {
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
update(boxUpdateInfo, callback);
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
@@ -572,12 +551,13 @@ function doUpgrade(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
@@ -586,7 +566,7 @@ function doUpgrade(boxUpdateInfo, callback) {
// no need to unlock since this is the last thing we ever do on this box
callback();
retire();
retire('upgrade');
});
});
}
@@ -601,7 +581,7 @@ function doUpdate(boxUpdateInfo, callback) {
progress.set(progress.UPDATE, 5, 'Backing up for update');
backupBoxAndApps(function (error) {
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
@@ -610,6 +590,7 @@ function doUpdate(boxUpdateInfo, callback) {
// this data is opaque to the installer
data: {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
@@ -635,7 +616,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %j', args);
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
superagent.post(INSTALLER_UPDATE_URL).send(args).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return updateError(error);
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
@@ -648,119 +629,31 @@ function doUpdate(boxUpdateInfo, callback) {
});
}
function backup(callback) {
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
// ensure tools can 'wait' on progress
progress.set(progress.BACKUP, 0, 'Starting');
// start the backup operation in the background
backupBoxAndApps(function (error) {
if (error) console.error('backup failed.', error);
locker.unlock(locker.OP_FULL_BACKUP);
});
callback(null);
}
function ensureBackup(callback) {
function installAppBundle(callback) {
callback = callback || NOOP_CALLBACK;
backups.getAllPaged(1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
}
var bundle = config.get('appBundle');
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
if (!bundle || bundle.length === 0) {
debug('installAppBundle: no bundle set');
return callback();
}
backup(callback);
});
}
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
function backupBoxWithAppBackupIds(appBackupIds, callback) {
assert(util.isArray(appBackupIds));
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
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) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
debug('backup: url %s', result.url);
async.series([
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey, result.sessionToken ]),
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
], function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('backup: successful');
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, result.id);
});
});
});
}
// this function expects you to have a lock
function backupBox(callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
backupBoxWithAppBackupIds(appBackupIds, callback);
});
}
// this function expects you to have a lock
function backupBoxAndApps(callback) {
callback = callback || NOOP_CALLBACK;
apps.getAll(function (error, allApps) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var processed = 0;
var step = 100/(allApps.length+1);
progress.set(progress.BACKUP, processed, '');
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
if (error && error.reason !== AppsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
if (error) {
progress.set(progress.BACKUP, 100, error.message);
return callback(error);
}
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error, restoreKey);
});
});
callback();
});
}
@@ -778,7 +671,7 @@ function checkDiskSpace(callback) {
var oos = entries.some(function (entry) {
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
(entry.mount === '/' && entry.available <= (1.25 * 1024 * 1024)); // 1.5G
});
debug('Disk space checked. ok: %s', !oos);
@@ -789,13 +682,76 @@ function checkDiskSpace(callback) {
});
}
function retire(callback) {
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function doMigrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error, backupId) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
options.restoreKey = backupId;
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function migrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CloudronError(CloudronError.BAD_FIELD, 'Not allowed in demo mode'));
if (!options.domain) return doMigrate(options, callback);
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint');
settings.setDnsConfig(dnsConfig, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
doMigrate(options, callback);
});
}
+3 -3
View File
@@ -1,6 +1,6 @@
LoadPlugin "table"
<Plugin table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.stat">
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat">
Instance "<%= appId %>-memory"
Separator " \\n"
<Result>
@@ -10,7 +10,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.max_usage_in_bytes">
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes">
Instance "<%= appId %>-memory"
Separator "\\n"
<Result>
@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker/<%= containerId %>/cpuacct.stat">
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
Instance "<%= appId %>-cpu"
Separator " \\n"
<Result>

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