Compare commits
2080 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5687b4bee0 | |||
| 48d0e73e9b | |||
| 3d4e3638be | |||
| f07e6b29a3 | |||
| a92f75f7d4 | |||
| 6e87111c99 | |||
| ad3594eebc | |||
| af99e31c63 | |||
| c8ee5b10be | |||
| cd471040b4 | |||
| f7beecc510 | |||
| ca8b61caba | |||
| d672b1e3f6 | |||
| 22ae39323b | |||
| 420a57aef9 | |||
| 7d76c32334 | |||
| 2fa4f4c66a | |||
| 37d146a683 | |||
| b95808be54 | |||
| dbdbdd9a2a | |||
| 16b8df7b9c | |||
| 293d4b4a47 | |||
| da7b2e62f5 | |||
| 33e87c7ffa | |||
| f417a35ad7 | |||
| c86acff698 | |||
| 0ec55b0cd4 | |||
| cf98d2a9d5 | |||
| ec75b14d9e | |||
| 4bad31f7cc | |||
| 288baa7e94 | |||
| d1161b3ff8 | |||
| 27e5886a0b | |||
| eaebf9fd73 | |||
| ea4c16604b | |||
| 66a4abeb50 | |||
| a57705264f | |||
| e7fc40cfdd | |||
| 55d306c938 | |||
| 8fe1f2fef1 | |||
| 1065b56380 | |||
| e58068688c | |||
| 9a51feed0a | |||
| 9ac8cc2cd7 | |||
| 54a388af5e | |||
| 5dda872917 | |||
| 3277cfdc6b | |||
| c759a1c3f6 | |||
| b77b2ab82d | |||
| 855de8565e | |||
| f1ad003b41 | |||
| f6507ecbe3 | |||
| 79083925d1 | |||
| de1c677e75 | |||
| 3ede9af34b | |||
| d475d9bcbf | |||
| bf095f0698 | |||
| 90d9d6da8b | |||
| 5ed4d66dfe | |||
| 60b45912ce | |||
| 29aad624d5 | |||
| 2bf8584f30 | |||
| d083ff3400 | |||
| b6e96d77aa | |||
| 6e1751d0ed | |||
| c1700069dc | |||
| 17c2aa4faf | |||
| 8f47861b6d | |||
| 8f2ee9a7cd | |||
| 93e976fdb0 | |||
| c737ea1954 | |||
| 700d815d54 | |||
| 382219a29f | |||
| a372853777 | |||
| 79f1cd16a3 | |||
| b2dbb5a100 | |||
| 01631e0477 | |||
| 816911d071 | |||
| 2cf0d6db9d | |||
| 1df47b7c05 | |||
| 622ac54213 | |||
| e2d8853704 | |||
| 4993c5010b | |||
| 8bd0d7c143 | |||
| 761ce99f8e | |||
| ba7c901d7a | |||
| 99c88ed7a0 | |||
| c27244cfbd | |||
| 099a42a2d4 | |||
| 74c89cf7d4 | |||
| 805125b17f | |||
| 7d93cfaac1 | |||
| 3cd1e7a972 | |||
| 4ed2651c5f | |||
| e83cb0fb3c | |||
| b1be65d9ce | |||
| eacc4412ba | |||
| 0baf092ba4 | |||
| ebd9249f87 | |||
| e1ee4973eb | |||
| ac09ad3393 | |||
| 2bba87d951 | |||
| d54e02eed4 | |||
| db41633663 | |||
| 0568387679 | |||
| ffbbb88917 | |||
| 756b36d227 | |||
| a2afadfe92 | |||
| 0c76cee737 | |||
| b1ec3fe271 | |||
| 19bf130ccd | |||
| 32c14e0aa1 | |||
| 0ff5050452 | |||
| ca83d4afb8 | |||
| 21c1591f58 | |||
| cb64ac1b7f | |||
| 337f808a62 | |||
| 48d97947c1 | |||
| df4dd4f93a | |||
| a5eb34d680 | |||
| eba03caa23 | |||
| 61a41a10ce | |||
| d3109022b1 | |||
| 1c828f19a3 | |||
| 2f1572b404 | |||
| 2ca12db362 | |||
| 14ef7688b8 | |||
| a1c83c79b2 | |||
| 376678881c | |||
| 0f7b11decd | |||
| 22b8540843 | |||
| afe5a1aa6c | |||
| 83b5bb394c | |||
| 539d430f60 | |||
| 6d898398df | |||
| 23a2077056 | |||
| d5bb797224 | |||
| 907bae53ba | |||
| 97122ed2be | |||
| 7b65529f63 | |||
| a87831b48c | |||
| baba7ca80d | |||
| d39a84ea53 | |||
| 3bcd255a07 | |||
| 67a87cd040 | |||
| be2aa70f7d | |||
| 2fac681b62 | |||
| dd4f7bf176 | |||
| 00a4b7ba09 | |||
| 51799f7f14 | |||
| 1b291365d5 | |||
| 9337f832d3 | |||
| ab540cb3e4 | |||
| 1adc47ab32 | |||
| 94037e5266 | |||
| 3457890b24 | |||
| b23c06d443 | |||
| f5ebb782c0 | |||
| 72f31744e3 | |||
| 2065a5f7f2 | |||
| 2ecf0c32cb | |||
| 9c0f2175f7 | |||
| 6064db9467 | |||
| 8cb8510d72 | |||
| 552ca43175 | |||
| 7c27f01ab8 | |||
| a8ec9a4329 | |||
| 797cf31969 | |||
| 37e365f679 | |||
| f53a9ab1aa | |||
| 4579de85bf | |||
| affc5ee7d9 | |||
| 40fa3818cc | |||
| 4a264ba8c5 | |||
| 8a47c36e20 | |||
| 2dc06a01b6 | |||
| f6695c9567 | |||
| fc3768101d | |||
| 5645954686 | |||
| f16d1c80f4 | |||
| a25b884dbb | |||
| 567401c337 | |||
| 1c80f3d667 | |||
| 17ebc67d36 | |||
| 4248776c16 | |||
| 3e0d6f698e | |||
| 67e2589a15 | |||
| 2398a515b5 | |||
| ad83d805ac | |||
| a6ba3535df | |||
| 3510d8f097 | |||
| d0100218c9 | |||
| 2cdeb40f33 | |||
| e033dce93e | |||
| 4c62338e97 | |||
| 606599a65b | |||
| d091ac4e0a | |||
| b676ebf9d7 | |||
| e270c27cb0 | |||
| 63561a51a4 | |||
| cde7599f87 | |||
| c9e7308f49 | |||
| 0088d9d5fc | |||
| 4fd5b369f8 | |||
| 5e0ed1dff3 | |||
| 215a16cd18 | |||
| cd5ae290bc | |||
| bd0b66aaad | |||
| 45b83232d7 | |||
| bf2885d7d3 | |||
| eeb8cc10ae | |||
| 4668e3a771 | |||
| 95a90dd050 | |||
| 908aa6f426 | |||
| 15f7ada958 | |||
| 18b58ced8d | |||
| 4f6f5bf3b7 | |||
| 50cbae420c | |||
| a1207de93f | |||
| a6824d8272 | |||
| 0eaeb67ba0 | |||
| b40a9803a8 | |||
| f1ab8fde76 | |||
| 55d11b2832 | |||
| e01da9b065 | |||
| b703dbd7f7 | |||
| c70c7462bf | |||
| 342dd26645 | |||
| 8e03295362 | |||
| 18cc3537d6 | |||
| 16deb001bf | |||
| 78035e0b2e | |||
| c23755c028 | |||
| 38ddf12542 | |||
| 525c7f2685 | |||
| 4d360e3798 | |||
| 8adf9f3643 | |||
| 6236a9c15e | |||
| cc6b260189 | |||
| 01953ded0f | |||
| 645dc21f7a | |||
| 34acb38d40 | |||
| 73918f8808 | |||
| 9f973133e8 | |||
| 5ba86d5c35 | |||
| 7b1b369e40 | |||
| 894384cf3c | |||
| 9768f8171c | |||
| 7672bc0c40 | |||
| 064c584b45 | |||
| 586fc4fe2d | |||
| ca22939298 | |||
| f8cc68b78d | |||
| 830972e8ae | |||
| 871f5728f8 | |||
| 3560af1b1e | |||
| 859d27522b | |||
| 9c90f88af4 | |||
| 8142ad3989 | |||
| 984c506c81 | |||
| 124c04167f | |||
| 105b8e0aeb | |||
| a22591a89f | |||
| c91464accc | |||
| d36af33269 | |||
| eaa747fe39 | |||
| 25243970ad | |||
| fc09cf2205 | |||
| e1be8659fa | |||
| eb963f3e1b | |||
| a983fb144f | |||
| a23f5d45b0 | |||
| e4b7b9c9fb | |||
| 0c6a2008ff | |||
| e7c82b3bf7 | |||
| 048f3e0614 | |||
| ae402f7afb | |||
| e848b23bc8 | |||
| 012fbe926f | |||
| e94cae88ab | |||
| d7a91429f3 | |||
| 254e0ef8e1 | |||
| 2e7cc4847e | |||
| 8cfc8bb893 | |||
| bd163327be | |||
| 9adc6d2ba5 | |||
| 5539710a25 | |||
| 6b6af13c5f | |||
| 6660ef2ff3 | |||
| 2ca5b3c197 | |||
| 049ab4d744 | |||
| dd9c594387 | |||
| 15cfbe3f99 | |||
| 0180dcf0ec | |||
| c8a04f8707 | |||
| 37185b1058 | |||
| f4aacfa2d0 | |||
| bc285a0965 | |||
| e9a35ec549 | |||
| 595787a898 | |||
| 235d969890 | |||
| 8efa75e5d6 | |||
| e700eb1551 | |||
| b7e36a6f33 | |||
| 30e91eb812 | |||
| 468e5e7e89 | |||
| 86a31b8f5a | |||
| b9ff8a2cef | |||
| e63ef4c991 | |||
| 1244a73a19 | |||
| 64f3b45eef | |||
| d494129353 | |||
| 0c3dda8ee0 | |||
| 3038521916 | |||
| d4d3eced56 | |||
| 2c279dc77e | |||
| 5d8b46e015 | |||
| 723c7307d2 | |||
| db55a7ad3c | |||
| 09b4325ecc | |||
| 66999f7454 | |||
| 2c511ccc5a | |||
| 6b72ee61f9 | |||
| 0a7303e50d | |||
| 906beaca29 | |||
| daf8250e44 | |||
| 4313d8a28c | |||
| 4fbce26877 | |||
| 702b93fe7c | |||
| 6755d13f1b | |||
| f80ce1778a | |||
| db7958c934 | |||
| 02e7c4eaef | |||
| ae299f5838 | |||
| bafc35f99e | |||
| 32eb1edead | |||
| 1187e6a101 | |||
| f94a653e80 | |||
| 1c22cb8443 | |||
| 49f7fb552b | |||
| d460c36e14 | |||
| 6e8eea6876 | |||
| fd1b56b9e9 | |||
| 92106a2a52 | |||
| 8809552fb2 | |||
| 3652d7f186 | |||
| 74abb26016 | |||
| 606f28c724 | |||
| 427f72fb24 | |||
| 21b28d3dcc | |||
| 1116bbe731 | |||
| 4099a7a32e | |||
| 97a17ff25f | |||
| 68d37b7260 | |||
| 7513817d41 | |||
| fadef230e9 | |||
| a672a930f8 | |||
| e6f8c83a6b | |||
| f8d50f6ea8 | |||
| 62b803624f | |||
| 9872ac424f | |||
| bca57b5e47 | |||
| e533f506cc | |||
| 0b8857e1bb | |||
| 5a1729d715 | |||
| 946d4f1b70 | |||
| 8e033dc387 | |||
| cf09f0995f | |||
| 19c7dd0de8 | |||
| 1d8df65fbf | |||
| 2be17eeb52 | |||
| 5c34cb24c6 | |||
| c12ee50b3b | |||
| c54a825eb8 | |||
| ef27a17cae | |||
| 8cf8661c2f | |||
| 7cdbab446d | |||
| 74ffd5c2d3 | |||
| 3a259e9ce0 | |||
| f9e47ac3c0 | |||
| 0c85f96b27 | |||
| b30300b8b2 | |||
| 6663a6bd66 | |||
| c1fc2ce095 | |||
| e614b930a5 | |||
| 9b4228f373 | |||
| 6e6d4f7413 | |||
| cac85b17bc | |||
| 449f8b03ad | |||
| 6eacc76281 | |||
| 33f764f6aa | |||
| 9ab845ef8a | |||
| eaee3ffbc9 | |||
| e1f268a325 | |||
| 1fc16d0fe8 | |||
| d7ea06e80e | |||
| 2d39a9bfa1 | |||
| f576f38e4c | |||
| 734506eb41 | |||
| 8ac8ea7d8a | |||
| 9d3f8f23ef | |||
| b0a8ba85e1 | |||
| 7e41ea9c31 | |||
| 1e65142f47 | |||
| f05a5226ba | |||
| c129328828 | |||
| acc644160a | |||
| c7e5c09bb9 | |||
| 1b3ae1f178 | |||
| bceeb092bf | |||
| 0d0229e531 | |||
| 629e061743 | |||
| d53657fa61 | |||
| 437c582be6 | |||
| 12ce714df4 | |||
| f09a1c577b | |||
| 4e3ba4c96f | |||
| 26c67d2d36 | |||
| 1e6b09c0da | |||
| 4ed74a8164 | |||
| 131cd96840 | |||
| fb4d6f7649 | |||
| da5e40db66 | |||
| 6c1c7e74c1 | |||
| 5a18c4dc26 | |||
| 0fbe2709ea | |||
| 6fdf5bd7ec | |||
| f2948483df | |||
| 1ef6eefaf6 | |||
| ae0f90c621 | |||
| 63a0c69e76 | |||
| 370e4f7c25 | |||
| 7cb8745029 | |||
| ba5f261f33 | |||
| 72f287c4e5 | |||
| c385abe416 | |||
| 49e3dba1f2 | |||
| e456c4b39c | |||
| 9b83a4d776 | |||
| 0ae1238233 | |||
| b45fca6468 | |||
| d7245b5e1e | |||
| 81c443d637 | |||
| 84e4c0033e | |||
| d7be1d7d03 | |||
| c8bf858ab0 | |||
| e2c206b755 | |||
| 882ed72f14 | |||
| 29451f8e07 | |||
| 29d3ad6cd3 | |||
| 4642d4c8c5 | |||
| ca7f26d5c7 | |||
| 98773160d0 | |||
| 6f0708eff2 | |||
| a2db4312b8 | |||
| 1e744c24f0 | |||
| 602265329d | |||
| 833e19a239 | |||
| 1a25ad77ca | |||
| 13e1b7060e | |||
| 3adf183569 | |||
| 8e3db8fa2e | |||
| 2c357e022b | |||
| 0f882614b1 | |||
| 3ae7a514ef | |||
| 7779e5da3b | |||
| cd0243d700 | |||
| ba588a1cd7 | |||
| f71b55c9e2 | |||
| d62cecff88 | |||
| 93fb01a9b9 | |||
| 39043736e5 | |||
| 475fd06ac0 | |||
| 1d12808b13 | |||
| 430ac330dc | |||
| 8e712da2c8 | |||
| 79d2b0c11c | |||
| 02e15dc413 | |||
| cf8282691b | |||
| 8c52221d26 | |||
| 450d644f71 | |||
| f9c6fbee72 | |||
| d5b50f48fd | |||
| cdf0b8c1b0 | |||
| 90aeeb3896 | |||
| 2ea772b862 | |||
| 1a4bb4d119 | |||
| 079bf3aed1 | |||
| 7c892706c3 | |||
| c1063112e8 | |||
| 7a07b52e7c | |||
| 08a45897c3 | |||
| 27d911addc | |||
| 441ea1af05 | |||
| 85c16ca43a | |||
| e1ef118d7b | |||
| 823e6575a6 | |||
| ec13938042 | |||
| 1a17627f83 | |||
| 61292c4df9 | |||
| 10ff0f559c | |||
| 601aa7f5cd | |||
| 36a91bb51a | |||
| 149c90e8f7 | |||
| c357efe4da | |||
| c43bc24a6a | |||
| a78e17b036 | |||
| cca9780f51 | |||
| 1d31975e2a | |||
| 7cb6961052 | |||
| 18e23e47df | |||
| ac469ddffc | |||
| a3401cdc3d | |||
| c6dc7d5c99 | |||
| 48e602273a | |||
| de25b34f71 | |||
| adc3c13a01 | |||
| b28c239dbf | |||
| b0c470da5a | |||
| 11cfa2efaa | |||
| 3a30310e2f | |||
| 08ae43ca13 | |||
| d426856883 | |||
| 9fb6a537ed | |||
| 58b5613c6b | |||
| ae9838a869 | |||
| 4204d76616 | |||
| 20b6df3cb8 | |||
| 6a4b60436e | |||
| e2b28d3286 | |||
| 7d5dfb64eb | |||
| 9111174b50 | |||
| f61842fc30 | |||
| a91ae2b9aa | |||
| 20708ad25a | |||
| c152580df0 | |||
| b9823fff44 | |||
| bd2848932e | |||
| 0327333be2 | |||
| a8861dd4f8 | |||
| 0c4a9d8bc9 | |||
| c1aa1eb33f | |||
| 0d3169c787 | |||
| 519dd2b889 | |||
| c9d5af8424 | |||
| a6547676a1 | |||
| 34f624abef | |||
| bd8acf763e | |||
| 4ba0504e7a | |||
| 2a7de5dab7 | |||
| ea87b3e876 | |||
| 23bf358bbe | |||
| 656356732e | |||
| 35a964bd00 | |||
| 5cff9df632 | |||
| 84de6c0583 | |||
| ca1c48b4b5 | |||
| 64278a9ff9 | |||
| 8bd790c1e0 | |||
| c9a0db0127 | |||
| a75cefa38f | |||
| 374f4be08f | |||
| 3fc17d38a5 | |||
| cfcf9f48cd | |||
| d26859acb4 | |||
| adcdd45053 | |||
| 33f803cd1c | |||
| 4856fc7de6 | |||
| 9d9278b6f2 | |||
| 7d7de9e900 | |||
| 4a37747cfe | |||
| 3e8cba08e3 | |||
| 703e76ceb6 | |||
| 577b509731 | |||
| 3c9beb1add | |||
| 46d8047599 | |||
| d39fa041bf | |||
| a7140412c4 | |||
| 3591452184 | |||
| a8d57bb036 | |||
| d92e99a092 | |||
| b40e740110 | |||
| cd500adfe4 | |||
| 55b80ac81f | |||
| 1f1f56b431 | |||
| baa2dbbf39 | |||
| 4b34f823a7 | |||
| c158548c19 | |||
| 8ce22c5656 | |||
| e4e54d87f2 | |||
| 2b1a94dc8d | |||
| afa352528f | |||
| 6a32f89bf2 | |||
| 49baad349c | |||
| 00ee2eea39 | |||
| 1d77c42269 | |||
| f24eee026e | |||
| 5773f26548 | |||
| 563b2a3042 | |||
| 565b0e13c8 | |||
| b863f3f89d | |||
| e3aeb4daf3 | |||
| 6480975ea7 | |||
| 5ebddf7df6 | |||
| 78367ea781 | |||
| 9bb4bf6eca | |||
| 54543aa536 | |||
| cdc337862f | |||
| 4d983f2a19 | |||
| 80b70bf0a9 | |||
| 505f4de55d | |||
| 4ee6a440fe | |||
| 52ae3e24d0 | |||
| 503a1d7229 | |||
| 9a000ddaf0 | |||
| 7fde57f7de | |||
| cf039b7964 | |||
| f552a8ac0d | |||
| c38abaa1c3 | |||
| 7b9eff94b3 | |||
| 4a9a6dc232 | |||
| 0bfc533e44 | |||
| b937a86426 | |||
| 6352064e6c | |||
| c9c1964e09 | |||
| 3ac786ba6d | |||
| e8be76f2e8 | |||
| 0ef9102b50 | |||
| 746afb2b21 | |||
| 02d1238853 | |||
| d8de9555f2 | |||
| f348fedd50 | |||
| 2a92d4772c | |||
| fa828cc661 | |||
| 04b7822be5 | |||
| 1fd96a847f | |||
| bf177473fe | |||
| 2ce768e29a | |||
| 96c8f96c52 | |||
| 83ed87a8eb | |||
| 5ac12452a1 | |||
| 6cecad89ec | |||
| 6c23bce8e8 | |||
| 73df6a8dd7 | |||
| be1cc76006 | |||
| 528f71ab0f | |||
| 6fa643049f | |||
| 835176ad75 | |||
| 56c272f34e | |||
| 98bb7e3a1a | |||
| 487fb23836 | |||
| cffc6d5fa5 | |||
| 1736d50260 | |||
| 982caee380 | |||
| 3cd7f47fbb | |||
| f5e71233c1 | |||
| 679c8a7d09 | |||
| 402c875874 | |||
| 5333311a35 | |||
| e2a22c3a5e | |||
| f251d4e511 | |||
| c39c1b9b51 | |||
| 28c8aa3222 | |||
| 056b3dcb56 | |||
| 9465c24c33 | |||
| f62bed5898 | |||
| 9b49c7ada7 | |||
| a40abaf1a0 | |||
| 7f2eadcd4e | |||
| c839e119b1 | |||
| 4a2e5ddc12 | |||
| c10302f146 | |||
| 8ef8f08b28 | |||
| 2ae4f76af5 | |||
| 12e2e64c22 | |||
| 10e7f27b16 | |||
| f3542dbd55 | |||
| c1bb264065 | |||
| ce19f480b3 | |||
| 839b4b11ba | |||
| 4df3b30ff0 | |||
| 471cfe1376 | |||
| 8de0746ac8 | |||
| cd94d8f433 | |||
| f2a1e19c9b | |||
| 217fcf564c | |||
| 55673ebcc3 | |||
| 1e22cc3236 | |||
| e40a2e8549 | |||
| a80302c4e0 | |||
| bbe3ddefc0 | |||
| 4405fcc652 | |||
| 3cb25ce6fd | |||
| ad34838f92 | |||
| e21df0ea92 | |||
| f72b683b76 | |||
| 95b27df635 | |||
| a4c2e5f3d2 | |||
| 3c5fadb1f5 | |||
| 486db676c9 | |||
| bde9279742 | |||
| efdd01c4c8 | |||
| f23ecd486d | |||
| b4c030b02b | |||
| a52f1e07ee | |||
| 186d0a1156 | |||
| 8e71046d28 | |||
| 67c56c7daf | |||
| d802b88998 | |||
| 2c9425ceea | |||
| 72a7d5e854 | |||
| fbf3a9daad | |||
| 1fc9e296b4 | |||
| cb31af828a | |||
| de9d556b9e | |||
| 9d98f9fcf5 | |||
| 5d3dca6b3f | |||
| 2ce6791771 | |||
| dd91de8cf6 | |||
| 1a0f3f687a | |||
| 36d48000b6 | |||
| b9c10a1256 | |||
| 5014ca7742 | |||
| 452c976aa6 | |||
| 5b9c8e517a | |||
| b66ba0a2c7 | |||
| 9a7ac4ffb7 | |||
| 408dd61408 | |||
| e915e6fd44 | |||
| ac5cef3c2f | |||
| aa3501c780 | |||
| 375082c1ae | |||
| 0900d7b824 | |||
| 1b54f5f797 | |||
| 9fcaebcf98 | |||
| fda8afa73d | |||
| 6beaa914d1 | |||
| b22fbfd381 | |||
| 84379cb11f | |||
| c63c6f793c | |||
| bc839d7f9b | |||
| 539b45d3b0 | |||
| 1098bbfe25 | |||
| 203cac2629 | |||
| 3aa2ccaef7 | |||
| 90472e1370 | |||
| ecc9d1bc02 | |||
| 4fc6eb1876 | |||
| e82152ac86 | |||
| 348e44e959 | |||
| ec63c1c96e | |||
| 59e1e55666 | |||
| 6b4d906336 | |||
| f96fda325d | |||
| 2caf57d2c7 | |||
| 01064323c2 | |||
| b7d2b644b3 | |||
| 3bdae8f4ac | |||
| cb14096fbd | |||
| 4d6fad79af | |||
| 4a827dcfb3 | |||
| 31636af643 | |||
| b4bd6500a3 | |||
| 7b4c9dffab | |||
| f083d81b35 | |||
| 550f14da6f | |||
| ec33dc99c0 | |||
| d07840c8ef | |||
| 23514078f3 | |||
| a5bb438af4 | |||
| 27a36492db | |||
| ef6344afae | |||
| 0058eddd22 | |||
| 464869c021 | |||
| b54ba8e511 | |||
| d40fb390b7 | |||
| 8af7ccfe08 | |||
| 59b53d347f | |||
| 70b63af3c9 | |||
| 94c3ac96f0 | |||
| 9f48f76185 | |||
| 5b295f8019 | |||
| 1874ea7f58 | |||
| 61ef3f3efb | |||
| 997152ad14 | |||
| 219bd69e63 | |||
| 35b25a4e28 | |||
| d3d7f2c320 | |||
| e25ad601c7 | |||
| a6a61d2586 | |||
| 42b65baa39 | |||
| 0eab262084 | |||
| 11b89f473e | |||
| 0e935580b6 | |||
| 6d783220fd | |||
| 344908b5b1 | |||
| 723de796c7 | |||
| 546d8ae4e2 | |||
| f3fd2d7950 | |||
| cbd4903960 | |||
| 267141fa9a | |||
| af82af5652 | |||
| d3a5a83f93 | |||
| 5b52eeb573 | |||
| db8afaf3ff | |||
| 8e76d44a30 | |||
| a86e30b917 | |||
| b214bd5d52 | |||
| 1f1e299939 | |||
| 8312cbe792 | |||
| a9210dcc0c | |||
| 8339e65eb8 | |||
| 3ba5bd836b | |||
| f1ed4ab20c | |||
| 22d86ff5b9 | |||
| d60b386bca | |||
| b7869a4fdd | |||
| 86903183df | |||
| e4c2483ae5 | |||
| 36f7e573a8 | |||
| 8bebbfbace | |||
| e198f34219 | |||
| 6a4bda1f7e | |||
| 3bf0a392b9 | |||
| 4165bf35d0 | |||
| fc1a288a2d | |||
| 7f37a9ce50 | |||
| d34f8bc082 | |||
| 50e598112d | |||
| 8150d1cb8f | |||
| 5b53280cd4 | |||
| 15e6873c14 | |||
| f3978897ae | |||
| ba4bb1fd90 | |||
| bbbc3837b0 | |||
| 311e997619 | |||
| 8ee2a7016d | |||
| 02c5e731a9 | |||
| b932a9be10 | |||
| 56618cab23 | |||
| 2f7fa54fc8 | |||
| b538c75f05 | |||
| 813950a0e5 | |||
| 8ef004f7f5 | |||
| 897326675e | |||
| 06a8508c48 | |||
| 979f63f3f8 | |||
| 55ba9a351f | |||
| bb6ee2b5a0 | |||
| b58b350827 | |||
| 3bd9fcae6a | |||
| 020ad746a0 | |||
| b049989eb1 | |||
| c25cc560d8 | |||
| d342652212 | |||
| c30cfefcc5 | |||
| 6cfb8226a9 | |||
| 19fad669f1 | |||
| 30074ae961 | |||
| 6d5dc0d5c4 | |||
| 7bc5ae17cc | |||
| 65994f307f | |||
| 855bc71ba7 | |||
| f3e842ed45 | |||
| 1ec5d8c03b | |||
| 26a590b827 | |||
| ed734ef2ae | |||
| 41ff92f747 | |||
| 8702b4320d | |||
| 6b4675cca1 | |||
| 15f94a5134 | |||
| 9c65fae4ec | |||
| 65b4c83b75 | |||
| 568c8fa100 | |||
| a91f89c7dd | |||
| dde597742c | |||
| 42fda25718 | |||
| 9fd40e506d | |||
| 2e51251cac | |||
| b0286a6f7f | |||
| 9a6e55e4ea | |||
| fc589a044d | |||
| 451c770b5c | |||
| c769af2bc3 | |||
| 4a5bb290a7 | |||
| 382aaf8de3 | |||
| c3f2b8b843 | |||
| 4b93d87310 | |||
| 4bb91be7d9 | |||
| 78d4fb3cb5 | |||
| 884fd5a224 | |||
| 28ee914828 | |||
| 2e9680ce68 | |||
| 124b952e88 | |||
| 1f1237e785 | |||
| d5644ae3f1 | |||
| c80b89ae8e | |||
| 6459c8792a | |||
| bf38bb30f3 | |||
| 33e572c49d | |||
| 46af8d1c90 | |||
| 30606a55fc | |||
| aedd370e76 | |||
| f60ff45cb6 | |||
| ce28449734 | |||
| f0ee52505c | |||
| 30e936263c | |||
| d0cf698dfa | |||
| df8910b1e1 | |||
| 7862fbd7ee | |||
| cd896a4422 | |||
| b8a635c638 | |||
| 690564983f | |||
| 4fb3a42319 | |||
| 6d2e52b3b5 | |||
| ced31afe55 | |||
| 5c4be56edb | |||
| 3595f624de | |||
| 0e9007e9ef | |||
| 971647c986 | |||
| 138829f69b | |||
| e0d4c1adc1 | |||
| 03c97d2027 | |||
| 867e875707 | |||
| 2ac7c15b90 | |||
| dcdca52dbd | |||
| 711814cc2f | |||
| c2b57a704d | |||
| 1106aa6bba | |||
| 482a87e994 | |||
| d65990f780 | |||
| 60c1fb4a93 | |||
| 02fcb749aa | |||
| dfc0598ec9 | |||
| b13dd55fc6 | |||
| 3020071fe4 | |||
| 57d2a3ff6e | |||
| 6fa414206c | |||
| 4619435a2d | |||
| ce433932dd | |||
| 4b79af7975 | |||
| 35cb804f00 | |||
| b132b2dc15 | |||
| 5da766131b | |||
| 642e5aceed | |||
| e8088be586 | |||
| 2a64764deb | |||
| a8d04028f3 | |||
| 57c7ae3c2b | |||
| 8165227b0a | |||
| f5af539102 | |||
| 41e1afaf68 | |||
| 7361acbec5 | |||
| adbe862fd3 | |||
| 7e3628f4c5 | |||
| 99af676344 | |||
| 9f377cb8fe | |||
| da1418c48b | |||
| 84b7d77aa0 | |||
| 748e30a6e5 | |||
| 34453c9dde | |||
| ebe64852be | |||
| f5c7e993ea | |||
| b628e2a6c8 | |||
| 01af6ef23a | |||
| 947edfec72 | |||
| 159fecc9ce | |||
| 0bf8b94bb4 | |||
| d4d07e27c0 | |||
| e9e09e66c3 | |||
| a67b2c7559 | |||
| d539f1fec8 | |||
| a3c270c4a1 | |||
| 33c70dad8b | |||
| 28ec9d82da | |||
| e13075b835 | |||
| 03022f0207 | |||
| 98facf2a3c | |||
| 338f4bcdea | |||
| e46b1a9245 | |||
| 129843c0ba | |||
| b079d688c1 | |||
| 684aec41cc | |||
| cc26c2b1f1 | |||
| 12915ee169 | |||
| e5a34581b1 | |||
| 5c53aec837 | |||
| b3a4973348 | |||
| b45fc46ff3 | |||
| 0c014d3e74 | |||
| 520845157f | |||
| 3193cec6aa | |||
| 17240c77bf | |||
| 82e8c8cef2 | |||
| 263c68f9c2 | |||
| 2ccbd7b8d1 | |||
| 3c6c575db9 | |||
| 3300c6b47a | |||
| 679d948857 | |||
| c00267a650 | |||
| 6d1a382381 | |||
| 8e2f259712 | |||
| 0a85f91175 | |||
| fe81cad9a2 | |||
| 3331d1aa13 | |||
| ae35c20227 | |||
| a49e1b5117 | |||
| 286f360908 | |||
| 7f6360361f | |||
| 0d5d54d2d8 | |||
| 37563ee8cb | |||
| e902e11024 | |||
| dcb14b452b | |||
| 66049a9e2d | |||
| 4b40084c7f | |||
| 33c701ece7 | |||
| cfeab2db42 | |||
| ebb564f623 | |||
| d501310dc3 | |||
| 0c4772db23 | |||
| 21c5033e34 | |||
| 46d725157f | |||
| b84ce23c12 | |||
| 75889af198 | |||
| c0f944c1bf | |||
| 743a8650f0 | |||
| 94ee636254 | |||
| 57d2fda14c | |||
| a26168e3cd | |||
| 5deadbfdc7 | |||
| 7b7e3b5950 | |||
| bcc1b6343e | |||
| d6e275aaf0 | |||
| 78f0992935 | |||
| f6dfb70afc | |||
| 11c530b605 | |||
| 61af079358 | |||
| 3335936e35 | |||
| 5a6b5f945d | |||
| 87d54b3883 | |||
| 32bce6a9a8 | |||
| fc932487e5 | |||
| 9ad4e61b87 | |||
| 44e7d87aac | |||
| 2637b740ab | |||
| 1caf4e9e76 | |||
| f0f01453ec | |||
| dc78aab821 | |||
| 9b4a400694 | |||
| d9b61500b3 | |||
| 2d01f2a0e9 | |||
| c3b9ed934d | |||
| b49d3bd639 | |||
| 9096e16e37 | |||
| 944b3a9da1 | |||
| 8d1ff3140a | |||
| 88fd25eff4 | |||
| 57332eb0ce | |||
| 6b16ce04ab | |||
| efba474aa5 | |||
| 0a45f087f2 | |||
| 95d7d9192d | |||
| bbe21d36c6 | |||
| 82a3ac5382 | |||
| 26ce8cf7ac | |||
| 812a6c7ea2 | |||
| 202af95502 | |||
| ff9fb1912b | |||
| f2c5d8d016 | |||
| eaeaf92c1a | |||
| 70034602c7 | |||
| dcc6108da1 | |||
| 6acd01eaae | |||
| e5baee82e8 | |||
| 1126626b51 | |||
| ab1b5f89a1 | |||
| 21c5491717 | |||
| fb5467d1cd | |||
| 3f5d974c0c | |||
| e422357670 | |||
| 53d03698ad | |||
| c8a3af83ff | |||
| 2e2b75bab2 | |||
| d259a3f326 | |||
| a92adf07f4 | |||
| ff428ba5bf | |||
| a5def529bb | |||
| 78fec9ec9b | |||
| bd5c1269f6 | |||
| 55e2043eca | |||
| bfd92bf7ed | |||
| 4983120ae8 | |||
| 200ae149a9 | |||
| a863b8fa22 | |||
| 9315e7eb65 | |||
| 982bfc313c | |||
| 4aa2ce4501 | |||
| 15d5ff1c51 | |||
| 505ede7f42 | |||
| 88b2ef65cc | |||
| 7fc1126e1f | |||
| 8f7e5f154b | |||
| 412243e656 | |||
| f06c218bd1 | |||
| 4149a5908b | |||
| e82c33b896 | |||
| 9182038d12 | |||
| da836d6bbe | |||
| 894d63554b | |||
| 568593db93 | |||
| 14983861c0 | |||
| f319919a4f | |||
| f2c897a87d | |||
| 9c8166a2b8 | |||
| 0642e64ccb | |||
| 77bd5bfcbe | |||
| 9a1392b784 | |||
| 4dabf7bb26 | |||
| 4250a26967 | |||
| 14ca94be78 | |||
| bcc3b4aee7 | |||
| 4d47c21a74 | |||
| c75b38ec56 | |||
| 64b59a3047 | |||
| 3a7eb74e28 | |||
| e64a85150a | |||
| 4939363296 | |||
| 4be3f484d0 | |||
| 9bfbdbba3b | |||
| 0c3de27c3d | |||
| 24e36dc24c | |||
| 1fb4c80951 | |||
| 43193a6394 | |||
| 66fd20e1ff | |||
| 84c5e7bdeb | |||
| c7c6944e5f | |||
| 823290aa29 | |||
| 118f36e115 | |||
| 2802d5f49b | |||
| ad9bb6555b | |||
| 3ec3f172bb | |||
| b9f0efa778 | |||
| 41e33e71c8 | |||
| ed5ebcbd5c | |||
| 914ebcb37d | |||
| 8769a1d15b | |||
| dac9f29900 | |||
| eaa2058b10 | |||
| 2131c6502c | |||
| 0ef1cd100a | |||
| 5a48b90adc | |||
| 701a9e964f | |||
| 621fb6ddce | |||
| d91fe9223c | |||
| 7826bc2b20 | |||
| 9a5e66739c | |||
| fd22f0d52b | |||
| 1bf869963b | |||
| d1dab8746e | |||
| 4adcd947e4 | |||
| b08618288a | |||
| f9ed725002 | |||
| 8cfbf92adc | |||
| eb93903bb8 | |||
| 501e1342b6 | |||
| 2a761a52d3 | |||
| ce116e56bf | |||
| ab9745e859 | |||
| ff4b1fa346 | |||
| 02fcee5d98 | |||
| 152589e7dd | |||
| cc3f21e213 | |||
| 61d8767c25 | |||
| 3416723129 | |||
| 6477c7b47d | |||
| 99ea4c8c30 | |||
| ef200fcc85 | |||
| c691b75344 | |||
| c24ef743f7 | |||
| 77ecf1ce22 | |||
| c6c36a4f3c | |||
| 2a3640032f | |||
| f0e8915825 | |||
| 96dabc5694 | |||
| abb3d5f0ef | |||
| 255d4ea088 | |||
| 3ac7992686 | |||
| 822e886347 | |||
| 7b5184f181 | |||
| f901728cc9 | |||
| 4f0132b371 | |||
| 3ffc2c0440 | |||
| f84de690ce | |||
| 9f74fead4b | |||
| c1c1fed605 | |||
| 87584be484 | |||
| 5a61c5ba51 | |||
| fbf7e6804f | |||
| dfa4d093f7 | |||
| acfeb85d4a | |||
| b3ad463470 | |||
| 1e54581c40 | |||
| a9b91591b4 | |||
| b59739ec54 | |||
| bb9bfd542d | |||
| 93b7e8d0a7 | |||
| b723f9e768 | |||
| 6bc14ea7e4 | |||
| cabed28f1e | |||
| ea74e389d3 | |||
| c37d914518 | |||
| 86b8fe324e | |||
| 8412db0796 | |||
| da9c07e369 | |||
| 3c305a51ce | |||
| 3ec29dc9e1 | |||
| a3d185e653 | |||
| c2da3da035 | |||
| 43c69d4dc6 | |||
| 5ba1dd39e7 | |||
| 56bc391b38 | |||
| 7e93c23110 | |||
| 7009b9f3ac | |||
| 0609a90d2a | |||
| fe62aba4d7 | |||
| 224a5f370f | |||
| 584df9a6da | |||
| f31c43bbc3 | |||
| 10ebff2edf | |||
| cc1755105c | |||
| 6c5a3997cb | |||
| 2017d668a9 | |||
| 7e57f31d14 | |||
| de0d42e52f | |||
| dbadbd2c4e | |||
| d51d2e5131 | |||
| be2c7a97b3 | |||
| 2ab13d587a | |||
| f13f24c88d | |||
| 91bc45bd4e | |||
| a75b9c1428 | |||
| d837e9f679 | |||
| d5ffa53e70 | |||
| fee6f3de0f | |||
| f15f3c9052 | |||
| a37f87511b | |||
| 069778caca | |||
| 741fe75def | |||
| f8b402a48e | |||
| f53c0b7700 | |||
| f74310f364 | |||
| a5a1526023 | |||
| 26f318477b | |||
| 060d9e88ef | |||
| 9cf497a87d | |||
| b174765992 | |||
| 9c2d217176 | |||
| 3197349058 | |||
| 5f3378878e | |||
| 53cd45496b | |||
| 942339435a | |||
| 2bd6519795 | |||
| 1763c36a0b | |||
| 2c0eb33625 | |||
| 040b9993c7 | |||
| 8f21126697 | |||
| 716d29165c | |||
| a2ec308155 | |||
| b82610ba00 | |||
| ed4674cd14 | |||
| 4e9dc75a37 | |||
| f284b4cd83 | |||
| 15cf83b37c | |||
| 0eff8911ee | |||
| 814a0ce3a6 | |||
| b3e1c221b7 | |||
| dc31946e50 | |||
| 36bbb98970 | |||
| ea4cea9733 | |||
| 7c06937a57 | |||
| 597704d3ed | |||
| 63290b9936 | |||
| 324222b040 | |||
| f37b92da04 | |||
| 0de3b8fbdb | |||
| f0cb3f94cb | |||
| 1508a5c6b9 | |||
| 9b9db6acf1 | |||
| 001bf94773 | |||
| 0160c12965 | |||
| d08397336d | |||
| 880754877d | |||
| 984a191e4c | |||
| cdca43311b | |||
| 020b47841a | |||
| 3f602c8a04 | |||
| dea0c5642d | |||
| 3d2b75860b | |||
| 0da754a14b | |||
| 3d408c8c90 | |||
| 40348a5132 | |||
| 9a177d9e46 | |||
| 6f1df9980d | |||
| 0c9d331f47 | |||
| f9db24e162 | |||
| 385bf3561b | |||
| 4304f20fe0 | |||
| 509083265f | |||
| 1b9dbd06c8 | |||
| d6482414bb | |||
| 194b9b35bd | |||
| 6b9acb4722 | |||
| 08c3cb9376 | |||
| 79631ba996 | |||
| 4776a005a5 | |||
| e954df2120 | |||
| 526a62a20e | |||
| e2432d002f | |||
| 6e4d6d1099 | |||
| fc2d1d61d7 | |||
| 4c4ae08b44 | |||
| 401c0e1b44 | |||
| e431bd6040 | |||
| a69cd204d6 | |||
| 3c3de6205e | |||
| 16444f775d | |||
| 2676658b5d | |||
| fbb8a842c1 | |||
| 62b586e8dd | |||
| 313d98ef70 | |||
| 06448f146d | |||
| 064d950f87 | |||
| 3236ce9cd6 | |||
| f74b22645f | |||
| 3540f2c197 | |||
| 3231fe7874 | |||
| dc8fd2eab3 | |||
| 3ae388602c | |||
| 733187f3c4 | |||
| 02d2a7058e | |||
| 25003bcf40 | |||
| 234caa60eb | |||
| a0227b6043 | |||
| 46ac6c4918 | |||
| 4afde79297 | |||
| 17d48f3fce | |||
| facdabcc8d | |||
| 691803f10b | |||
| 8144c6d086 | |||
| 290ab6cc7d | |||
| 8e5af17e5d | |||
| d9d94faf75 | |||
| 0201ab19e4 | |||
| 721fe74f3c | |||
| 96eeb247a1 | |||
| 6261231593 | |||
| d62d2b17fe | |||
| 89cef4f050 | |||
| 8602e033c5 | |||
| 3598d89b12 | |||
| ffd552583c | |||
| 9eabc9d266 | |||
| edf8cd736e | |||
| c5ebe2c2bf | |||
| 5d0ccc0dd7 | |||
| 4147455654 | |||
| f3436a99a2 | |||
| 70d569e2e8 | |||
| 684625fbaf | |||
| c8b9ae542c | |||
| af29c1ba86 | |||
| 207e81345f | |||
| d880731351 | |||
| e603cfe96e | |||
| 5b93a2870f | |||
| 1214300800 | |||
| 8159334cbf | |||
| 78135c807a | |||
| bfa33e4d8e | |||
| 8b23174769 | |||
| a078c94b97 | |||
| c86392cd60 | |||
| f0e9256d46 | |||
| 0cd4e4f03a | |||
| 1766da9174 | |||
| dbdcf1ec27 | |||
| c916ea2589 | |||
| 5540b5f545 | |||
| 1e38190e68 | |||
| 8f3553090f | |||
| cc0f5a1f03 | |||
| a1c531d2a8 | |||
| 57cb3b04d7 | |||
| a49cf98a8d | |||
| da6cab8dd6 | |||
| 3b7cfdd7db | |||
| f9251c8b37 | |||
| 4068ff5f21 | |||
| ee073c91a3 | |||
| 9e8742ca87 | |||
| 7f99fe2399 | |||
| bfe8df35df | |||
| e2848d3e08 | |||
| bc823b4a75 | |||
| c24f780722 | |||
| 0d51ec9920 | |||
| e07e544029 | |||
| 5aff55c5ca | |||
| 5ebc29746d | |||
| 8fc44e6bc9 | |||
| 44f4872134 | |||
| 49dd584a41 | |||
| 6d8f1f90d4 | |||
| c1ded66c1a | |||
| 4df49a82e5 | |||
| 92e6ee9539 | |||
| 3ad2a2a5ca | |||
| 226537de04 | |||
| 41b324eb2d | |||
| 1360729e97 | |||
| 725e1debcc | |||
| 201efa70b7 | |||
| c52d0369fa | |||
| b4dfad3aa3 | |||
| 7667cdc66d | |||
| 3a9a667890 | |||
| 304cfed5a9 | |||
| 778c583a52 | |||
| f988bb4d14 | |||
| 7057f1aaa2 | |||
| e06f5f88b8 | |||
| 03cd3f0b6f | |||
| 615f875169 | |||
| f27ba04a00 | |||
| 3e0006a327 | |||
| 558ca42ae8 | |||
| 9d8a803185 | |||
| 105047b0c4 | |||
| e335aa5dee | |||
| 10163733db | |||
| 251fad8514 | |||
| 036740f97b | |||
| f4958d936c | |||
| 80ca69a128 | |||
| 097d23c412 | |||
| 13a1213b0d | |||
| 76fe2bf531 | |||
| 50c4e4c91e | |||
| 46441d1814 | |||
| a4e73be834 | |||
| 6be0d0814d | |||
| e30d71921e | |||
| a49c78f32c | |||
| b077223e58 | |||
| d2864dfe56 | |||
| 6d08af35a8 | |||
| 54f9d653f7 | |||
| 8d65f93fa4 | |||
| 462440bb30 | |||
| 65261dc4d5 | |||
| 54ead09aac | |||
| 28b3550214 | |||
| e2e70da4c5 | |||
| 7326ea27ca | |||
| 1fe00f7f80 | |||
| e9e9d6000d | |||
| 6dccb3655f | |||
| c3113bd74d | |||
| e79119b72a | |||
| 086cfdc1e6 | |||
| 1f091d3b4b | |||
| 892fa4b2ec | |||
| a87b4b207c | |||
| bdd14022d6 | |||
| 3d40cf03b1 | |||
| 594be7dbbd | |||
| a52e2ffc23 | |||
| 8eeee712aa | |||
| 0f62faa198 | |||
| bfd66cf309 | |||
| c2f7d61e34 | |||
| d5d5e356ae | |||
| 531752cd43 | |||
| 9eac56578c | |||
| d06398dbfd | |||
| 60ce6b69ee | |||
| 4fcc7fe99f | |||
| 82cd215ffa | |||
| 1dcea84068 | |||
| 4107252bfe | |||
| 9cc6cb56f7 | |||
| 48b99a4203 | |||
| 824767adbb | |||
| 3d84880d92 | |||
| dfa08469d6 | |||
| d798073d95 | |||
| 41632b8c11 | |||
| 6ccc46717e | |||
| 2495caf2eb | |||
| ae9c104a8b | |||
| 683f371778 | |||
| eb29bdd575 | |||
| b13de298bf | |||
| 47978436c2 | |||
| 71b5cc4702 | |||
| 5a9e32d41a | |||
| b03e4db8d5 | |||
| 663ff2410a | |||
| f763759008 | |||
| 69aa11d6c6 | |||
| 65041743c5 | |||
| 76214d3d7a | |||
| be83a967fc | |||
| 119e095710 | |||
| 5df3a41988 | |||
| a34b611e20 | |||
| 75c1731443 | |||
| 9e36b7abf4 | |||
| b37226d4d1 | |||
| 311efe5d10 | |||
| ebdd6d8a31 | |||
| 3ee9f70113 | |||
| adfc069e16 | |||
| 31fd0d711a | |||
| a6a852cfae | |||
| e9b3e22e86 | |||
| 564d61bcf5 | |||
| 5582ac7402 | |||
| a05b6ad78d | |||
| ec71390d0b | |||
| 68a3862ee5 | |||
| a9f70d8363 | |||
| e91539d79a | |||
| 5546bfbf0e | |||
| 803d47b426 | |||
| e4c0192243 | |||
| d5b5289e0c | |||
| 2909aad72a | |||
| cafbb31e78 | |||
| 080128539c | |||
| cf93a99a4e | |||
| ce927bfa22 | |||
| 6993a9c7e7 | |||
| 84d04cce16 | |||
| f735fd8172 | |||
| 53e28db1d6 | |||
| 77457d1ea9 | |||
| 161b7cf76b | |||
| 01b6defd24 | |||
| badc524ff2 | |||
| b3f53099f0 | |||
| a28560cdc0 | |||
| 4afdf50736 | |||
| 078e36f07f | |||
| 3b8e15a61c | |||
| 67682c5d27 | |||
| 48e3b8ebf9 | |||
| 2072dedf66 | |||
| 51f43ecc27 | |||
| 2347a7ced2 | |||
| b2cadaf95c | |||
| 957f787701 | |||
| ad48067bb2 | |||
| 12b6c46558 | |||
| b4ba17c599 | |||
| 7fb28662c1 | |||
| 4845db538a | |||
| 8429985253 | |||
| aff9ff47bc | |||
| 9b3077eca3 | |||
| 39396cb3ab | |||
| 364f0ead51 | |||
| 5ac1d5575c | |||
| e5a030baff | |||
| a100837e69 | |||
| ffacf17a42 | |||
| f5d37b6443 | |||
| d71d09c1ba | |||
| c1a2444dfa | |||
| ef40aae3ba | |||
| 9570086c87 | |||
| 57a823a698 | |||
| ec0ee07b17 | |||
| 3d7545133e | |||
| bcc752469a | |||
| da85f4c096 | |||
| 3b740a5651 | |||
| 7eb202f19a | |||
| 8dbd4c8527 | |||
| 88f2ce554d | |||
| 57888659a6 | |||
| ebdefa7f18 | |||
| 569150f602 | |||
| 6ccb806628 | |||
| ae807b28b6 | |||
| 00726b01e2 | |||
| f5b777ab33 | |||
| d84e584222 | |||
| 31e452e1cc | |||
| e015b9bd7a | |||
| 10e0cbcebc | |||
| 2768c3a336 | |||
| 37512c4cac | |||
| 0aaaa866e4 | |||
| 53cb7fe687 | |||
| da42f2f00c | |||
| 27d2daae93 | |||
| 42cc8249f8 | |||
| de055492ef | |||
| efa3ccaffe | |||
| 32e238818a | |||
| 2c1083d58b | |||
| 517b967fe9 | |||
| 3c4ca8e9c8 | |||
| 4ec043836b | |||
| 7ec93b733b | |||
| a81262afb5 | |||
| 266603bb19 | |||
| 6dcecaaf55 | |||
| 099eb2bca4 | |||
| b92ed8d079 | |||
| 0838ce4ef8 | |||
| cc8767274a | |||
| dfaed79e31 | |||
| 9dc1a95992 | |||
| 5be05529c2 | |||
| 6ff7786f04 | |||
| a833b65ef3 | |||
| 83a252bd20 | |||
| 7ef3805dbc | |||
| e5d906a065 | |||
| b5e4e9fed6 | |||
| 3ccb72f891 | |||
| 45cd4ba349 | |||
| 5b9b21c469 | |||
| ed55ad1c6f | |||
| 560f460a32 | |||
| aa116ce58c | |||
| 3f0e2024e4 | |||
| d9c5b2b642 | |||
| 5322ed054d | |||
| 39c4954371 | |||
| 78ad49bd74 | |||
| f56c960b92 | |||
| 8e077660c4 | |||
| 1b8b4900a2 | |||
| 27ddcb9758 | |||
| fdb951c9e5 | |||
| 0f2037513b | |||
| 9da4e038bd | |||
| ae3e0177bb | |||
| 0751974624 | |||
| b8242c82d6 | |||
| 442c02fa1b | |||
| d5306052bb | |||
| 8543dbe3be | |||
| bf42b735d1 | |||
| a2ba3989d0 | |||
| 6f36d79358 | |||
| 1da24564b3 | |||
| da61d5c0f1 | |||
| 79da7b31c7 | |||
| 631b238b63 | |||
| ff5ca617b1 | |||
| e16125c67e | |||
| 646ba096c3 | |||
| 8be3b4c281 | |||
| 5afff5eecc | |||
| 84206738e1 | |||
| 8b2e4ce700 | |||
| 776f184dbc | |||
| a54466f8c2 | |||
| f36641b443 | |||
| 36eb107b83 | |||
| fa16ae9a0c | |||
| 517b36b3f0 | |||
| 83a28afc8f | |||
| e76c7de259 | |||
| fcda4a771c | |||
| 76d8f16e22 | |||
| 1b3cd1f373 | |||
| 62b020e96d | |||
| bc78f4a6d8 | |||
| a8e458e935 | |||
| e4747ef50c | |||
| 0d6637de27 | |||
| 28e513a434 | |||
| e73174685b | |||
| 3af95508f5 | |||
| 4fa8ab596b | |||
| 54c9bb7409 | |||
| 4c7dc5056d | |||
| e986a67d39 | |||
| da8de173a6 | |||
| cbc906f8d1 | |||
| c7958f8e1d | |||
| b88ee8143a | |||
| e413f7ba9b | |||
| 7e1055ae44 | |||
| c61ce40362 | |||
| e48156dceb | |||
| f3811e3df9 | |||
| 0d40b1b80d | |||
| 8b92c8f7ae | |||
| d41eb81b3d | |||
| 3adf91afed | |||
| 18f05de8ae | |||
| b0f4396389 | |||
| bf99475dbd | |||
| d50fa70f47 | |||
| 0e655cadb0 | |||
| 496e1c3dc1 | |||
| 325252699e | |||
| 2d43e22285 | |||
| 9e673c3890 | |||
| c3c18e8a4b | |||
| cb1bd58cb9 | |||
| 0bdff14c9f | |||
| c4ae9526af | |||
| 8d79ac9ae0 | |||
| 7d4ed5bafc | |||
| 85db8f398b | |||
| 636b71ce6f | |||
| b46008f0b1 | |||
| 9d9bd42cd2 | |||
| 0f6a2a42f2 | |||
| fc8bf82993 | |||
| e56192913d | |||
| 2d08ce441f | |||
| 87497c2047 | |||
| 32a0bf6fd2 | |||
| 291e625785 | |||
| 6bcfd33e10 | |||
| b4c15b1719 | |||
| 920626192c | |||
| 778371b818 | |||
| d2a1cea1e8 | |||
| 5683cefe89 | |||
| 126d64ffa8 | |||
| 7262eb208f | |||
| f57f8f5e58 | |||
| 00ad1308aa | |||
| 2cc37a9c31 | |||
| 13b0093f20 | |||
| 5617baea50 | |||
| da79e4f229 | |||
| 6c1bd522e6 | |||
| 7fca9decc1 | |||
| 7cf08b6a4d | |||
| ffedbdfa13 | |||
| 43e207a301 | |||
| bea6019dc4 | |||
| 9042e9e1a9 | |||
| b855dee4cb | |||
| b322f6805f | |||
| ccc119ddec | |||
| 994cbaa22a | |||
| d7a34bbf68 | |||
| 1f31fe6f8f | |||
| 37bdd2672b | |||
| 63702f836a | |||
| 5898428a6c | |||
| f4a6c64956 | |||
| f9d4d3014d | |||
| 8254337552 | |||
| d811115f21 | |||
| fec388b648 | |||
| a969e323a6 | |||
| 09584ac29c | |||
| 7967610f3f | |||
| 727332fe66 | |||
| 09595d1c43 | |||
| c4ad6c803f | |||
| fd1a00d280 | |||
| 5c2a650681 | |||
| 43051cea3b | |||
| 3d50a251ee | |||
| 219df8babd | |||
| d3d9706a70 | |||
| 8a3ad6c964 | |||
| 90719cd4d9 | |||
| 30445ddab9 | |||
| 157fbc89b8 | |||
| 71219c6af7 | |||
| 934abafbd4 | |||
| bc6e896507 | |||
| ca8731c282 | |||
| c511019d79 | |||
| 992c4ee847 | |||
| 8c427553ba | |||
| c1df22f079 | |||
| 7673ecde2f | |||
| a9d0cf66fd | |||
| db89784af8 | |||
| 4143f903ad | |||
| 12820db4a5 | |||
| 8837cc5a3c | |||
| f2545e3def | |||
| 7a72bf3f78 | |||
| 87351f04ef | |||
| 4a04e0b52f | |||
| 5945bce00e | |||
| d2a3925e04 | |||
| 9c9f82e2c5 | |||
| 8fd3ff0ccc | |||
| dec2fdb6bb | |||
| c581d2a52c | |||
| e6e748e30d | |||
| 36fddacf5c | |||
| 183c1608a6 | |||
| 51c8f65e8d | |||
| 0da6e9a5b9 | |||
| 31c7a17684 | |||
| a1e2cd438e | |||
| 6b0e00e28b | |||
| 19948851e0 | |||
| bfe4f75881 | |||
| 7f13594f01 | |||
| 4fafac035e | |||
| ca41e6acfd | |||
| 9893dd6640 | |||
| 8f7e4c2053 | |||
| d037b13401 | |||
| 83c955d25b | |||
| 0bb6d969a4 | |||
| 6062a5bdd2 | |||
| 70ab492efa | |||
| aab035f7b9 | |||
| 0e825272ae | |||
| 46fee9e431 | |||
| 0789c96992 | |||
| a4adc581fa | |||
| 500fb452e7 | |||
| e11b762ea1 | |||
| f5d1726352 | |||
| 3d5aa9fd23 | |||
| ef12740060 | |||
| 415902d68e | |||
| 0ef0e010a3 | |||
| 2d27da89d2 | |||
| 9d8def8349 | |||
| 2533111bfa | |||
| 20d6da8230 | |||
| f159cacfbb | |||
| 5e9ea98b66 | |||
| d87b7dcb75 | |||
| 6eea2fef9a | |||
| 34fd5f14a5 | |||
| a4e73e747c | |||
| eadff099eb | |||
| 15653cb3f8 | |||
| 2f8dc35c5d | |||
| a97720d204 | |||
| 73898505b0 | |||
| 88b4b6a38b | |||
| 3da82e3a63 | |||
| dad1585704 | |||
| e81dbdb36c | |||
| ee2478e500 | |||
| 0f7a6964a4 | |||
| 5fa974ffe6 | |||
| e1b7198a29 | |||
| 37d6354627 | |||
| 6ab3e04fc1 | |||
| b1987868be | |||
| 72eb3007c4 | |||
| 6c1da45ad1 | |||
| b9857cdb65 | |||
| d5c251115c | |||
| 64c66e248b | |||
| bb53c4f331 | |||
| 3215d4a3c9 | |||
| 68c4d77494 | |||
| 44bf299e10 | |||
| 6b1e14b464 | |||
| 8dcde84c3c | |||
| a0deedb958 | |||
| a2096bec18 | |||
| 4f82bcec43 | |||
| 491356ce8d | |||
| 6c99105a7e | |||
| 71f847776b | |||
| 87c5371603 | |||
| 01d676628d | |||
| 60badce935 | |||
| 182ae6bf1f | |||
| c62ef9e156 | |||
| 96383a1fae | |||
| 5e9542ee76 | |||
| cc28d49df4 | |||
| 18f3733d6e | |||
| 87dcf42c7e | |||
| 32d8627045 | |||
| 6a607f9565 | |||
| c623770b44 | |||
| 69f3620b22 | |||
| 21110bb2e0 | |||
| fabe55622e | |||
| 73e079cc6c | |||
| a7d22a1972 | |||
| 5c1970b37f | |||
| db065bd0fc | |||
| db6d8deec4 | |||
| 414b21f29a | |||
| c4c7668b5a | |||
| b9fa87cca2 | |||
| 218c9099fd | |||
| 916d97f7bd | |||
| 109f777c00 | |||
| 4bf3a78227 | |||
| c03e69232e | |||
| 91a016ee91 | |||
| 8256f97e9d | |||
| d095899aef | |||
| 6293c0aede | |||
| 101ce62ef3 | |||
| 9f443e2d07 | |||
| 0a30585a05 | |||
| ed78bd05c8 | |||
| c24d7e7b3c | |||
| 389d2be82d | |||
| 38b85e6006 | |||
| de2cde7333 | |||
| 08410569c0 | |||
| be3b08a7b4 | |||
| 2724cfd0ad | |||
| d7c8cf5e0e | |||
| 11f89da3a0 | |||
| a803af2300 | |||
| 6991402a8c | |||
| 259798a8f2 | |||
| d83395ecfb | |||
| 6d3dd452be | |||
| 40bee79e3d | |||
| 95de25560b | |||
| 79eee94a5e | |||
| 82651a33c7 | |||
| 212a0ffcd9 | |||
| 115ed12c36 | |||
| 53268b67dc | |||
| 40dd12ba68 | |||
| 7a111e29ad | |||
| 065c65317d | |||
| 91a5d711f4 | |||
| 9071ea6c5e | |||
| 34521735da | |||
| b7f6dfb197 | |||
| fa330b4652 | |||
| 3bdbcff811 | |||
| ea3bd6d71d | |||
| d5cc96b1ff | |||
| 4ed368cdd8 | |||
| 5229222014 | |||
| 9b0aa331e1 | |||
| 70cc073b1c | |||
| 29502fd8af | |||
| 8d75fcfe67 | |||
| b2668579d6 | |||
| ba663faa64 | |||
| 8db76f6b70 | |||
| 322e9faee7 | |||
| af9d489395 | |||
| 4565291c1c | |||
| be127ec313 | |||
| 8b3a44b33c | |||
| 08b5d7003d | |||
| 60cc4c988f | |||
| 68219748ec | |||
| cfb56d7eee | |||
| 4690616230 | |||
| 96d625b866 | |||
| 2e281f8554 | |||
| 5da5d86bc8 | |||
| 103c0bd688 | |||
| 275d8c2121 | |||
| 4c964bcaf8 | |||
| e6c2c77f03 | |||
| 819095b465 | |||
| 1453fd3c54 | |||
| 867278a0b6 | |||
| 382fca3cf2 | |||
| f210501e12 | |||
| 499921e3af | |||
| db19df9395 | |||
| 6e2067bfe7 | |||
| 8eb1b374ef | |||
| 1734555974 | |||
| 7136de4d08 | |||
| 21e8bc1ce5 | |||
| 13020be6e6 | |||
| 3b922ff8b2 | |||
| 69402d0079 | |||
| 99850f1161 | |||
| b205212bf2 | |||
| baf586b028 | |||
| 94faa3575c | |||
| 544c1474d1 | |||
| bb25279878 | |||
| 4939f526d5 | |||
| 68af03f401 | |||
| f744fee708 | |||
| c7ceb29845 | |||
| 56d9d5913d | |||
| f7887228d3 | |||
| 73ed0384ea | |||
| 3051d4c22a | |||
| b32a0bcfad | |||
| 61c79aab23 | |||
| 9740ffd504 | |||
| 435ec2365b | |||
| ff3562b0e8 | |||
| 3be5511e33 | |||
| c8604e95ab | |||
| bbaf4c77fd | |||
| 1c9fc3f3dc | |||
| 577959f281 | |||
| 8af01f2955 | |||
| c73213b2f2 | |||
| 36f3f4b8f4 | |||
| 31bd5cdee3 | |||
| fd0326efb1 | |||
| 65c6806109 | |||
| 1b7406784e | |||
| 8cbf83058f | |||
| e058e22cae | |||
| c84674529b | |||
| a0098a8883 | |||
| f6547c9b71 | |||
| 6dc17183ee | |||
| bba3dd5ec0 | |||
| 9eec6c2e9d | |||
| c235b82660 | |||
| 67ac0fcd5a | |||
| 87ca147e65 | |||
| 0cf2bfb792 | |||
| a112e614e6 | |||
| 0b1dcd2940 | |||
| 951934f275 | |||
| 78518ff5f6 | |||
| b8d0c01187 | |||
| 572e5c4938 | |||
| e4fabd20c1 | |||
| 726d154890 | |||
| 7a5ac1a2f5 | |||
| c90a8041e2 | |||
| 18b91b5fa0 | |||
| f058c266d2 | |||
| e0114c87ac | |||
| c98275000b | |||
| 553509c462 | |||
| 306bef96b4 | |||
| 497eaea65e | |||
| 8aacc503a6 | |||
| ec160fe45f | |||
| 82c74e6787 | |||
| bbff195863 | |||
| e528dbcfc0 | |||
| 0467e80c71 | |||
| c9ef0056e0 | |||
| efb228cf5e | |||
| af700827c5 | |||
| 3135783fe3 | |||
| 496f530b9f | |||
| f44c2707f0 | |||
| 9fbbddc3eb | |||
| 5afb16aa98 | |||
| 8f2b0bae5e | |||
| fcfd1dceac | |||
| d839f0b762 | |||
| 16a65fb185 | |||
| aaeb355183 | |||
| c236072c4c | |||
| 5d92cff638 | |||
| 1b539b8d22 | |||
| a21a913f34 | |||
| 357f6f0552 | |||
| b16aa4c007 | |||
| 1fed5ee353 | |||
| 29077abf7c | |||
| f5c7116573 |
@@ -1,7 +1,6 @@
|
||||
# following files are skipped when exporting using git archive
|
||||
/release export-ignore
|
||||
/admin export-ignore
|
||||
test export-ignore
|
||||
docs export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
node_modules/
|
||||
coverage/
|
||||
docs/
|
||||
webadmin/dist/
|
||||
setup/splash/website/
|
||||
installer/src/certs/server.key
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -444,6 +444,250 @@
|
||||
- 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)
|
||||
|
||||
[0.20.3]
|
||||
* Make DigitalOcean selfhosting independent
|
||||
|
||||
[0.21.0]
|
||||
* Delivery of email to aliases is now case insensitive (#35)
|
||||
* Mailing list support via Groups (#15)
|
||||
* Fix issue where non-admin users could not update their profile
|
||||
|
||||
[0.21.1]
|
||||
* Fix app clone error (mailbox was not allocated)
|
||||
* Do not allow "-" in group names
|
||||
|
||||
[0.22.0]
|
||||
* Rebuild server instances instead of recreating
|
||||
|
||||
[0.50.0]
|
||||
* Add UI to configure backup location
|
||||
* Add DNS backend to make it easy to run on any server with SSH access
|
||||
* Update wildcard certificate
|
||||
* Fix crash in mail container with SPF plugin
|
||||
* Fix postgresql addon to restore correctly
|
||||
* Periodically cleanup file system backups
|
||||
* Improve invitation emails
|
||||
* Fix bug where mailbox name was generated incorrectly for nake domain (#81)
|
||||
|
||||
[0.60.0]
|
||||
* Implement new approach to selfhosting. `cloudron machine create` is now deprecated.
|
||||
Please see the [selfhosting guide](https://cloudron.io/references/selfhosting.html)
|
||||
for more details
|
||||
* Send email to admins if backup fails
|
||||
* Add UI to set digitalocean as DNS provider
|
||||
|
||||
[0.60.1]
|
||||
* Apply less strict hostname checking for email
|
||||
* Fix bug in Cloudron plan listing
|
||||
* Improved storage provider interface
|
||||
|
||||
[0.70.0]
|
||||
* Remove standalone installer daemon
|
||||
|
||||
[0.70.1]
|
||||
* Add additional platform healthcheck
|
||||
|
||||
[0.80.0]
|
||||
* Add optional SSO for apps
|
||||
* Improve app status page
|
||||
* Several webinterface improvements
|
||||
|
||||
[0.80.1]
|
||||
* Improved DNS handling
|
||||
* Better error messages in UI
|
||||
|
||||
[0.90.0]
|
||||
* Remove customAuth support
|
||||
* Support non AWS S3 object storage
|
||||
* Settings UI improvements
|
||||
|
||||
[0.91.0]
|
||||
* Support installing Cloudron on intranet and VirtualBox
|
||||
* Fix bug where relocating an app did not free the old location
|
||||
* Allow Email server to be enabled with wildcard DNS
|
||||
|
||||
[0.92.0]
|
||||
* Backup encryption key is now optional
|
||||
* Fix bug where DNS mail record warning was shown by mistake
|
||||
* Make cloudron-setup finish with `manual` DNS provider
|
||||
|
||||
|
||||
@@ -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/>.
|
||||
@@ -1,17 +1,79 @@
|
||||
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.
|
||||
Support us on
|
||||
[](https://flattr.com/submit/auto?user_id=cloudron&url=https://cloudron.io&title=Cloudron&tags=opensource&category=software)
|
||||
or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8982CKNM46D8U)
|
||||
|
||||
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
|
||||
## Features
|
||||
|
||||
* 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
|
||||
@@ -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}"
|
||||
|
||||
@@ -5,8 +5,8 @@ set -euv -o pipefail
|
||||
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 INSTALLER_REVISION="${1:-master}"
|
||||
readonly PROVIDER="${2:-generic}"
|
||||
readonly USER_DATA_FILE="/root/user_data.img"
|
||||
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
@@ -17,40 +17,19 @@ 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
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
echo "=== Yellowtent base image preparation (installer revision - ${INSTALLER_REVISION}) ==="
|
||||
|
||||
echo "=== Prepare installer source ==="
|
||||
rm -rf "${INSTALLER_SOURCE_DIR}" && mkdir -p "${INSTALLER_SOURCE_DIR}"
|
||||
rm -rf /tmp/box && mkdir -p /tmp/box
|
||||
tar xvf /tmp/box.tar.gz -C /tmp/box && rm /tmp/box.tar.gz
|
||||
cp -rf /tmp/box/installer/* "${INSTALLER_SOURCE_DIR}"
|
||||
echo "${INSTALLER_REVISION}" > "${INSTALLER_SOURCE_DIR}/REVISION"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Upgrade ==="
|
||||
apt-get update
|
||||
apt-get dist-upgrade -y
|
||||
apt-get install -y curl
|
||||
apt-get -o Dpkg::Options::="--force-confdef" update -y
|
||||
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
|
||||
apt-get install -y curl iptables
|
||||
|
||||
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
|
||||
# Do NOT use iptables-persistent because it's startup ordering conflicts with docker
|
||||
@@ -59,22 +38,23 @@ echo "=== Setting up firewall ==="
|
||||
iptables -F # flush all chains
|
||||
iptables -X # delete all chains
|
||||
# default policy for filter table
|
||||
iptables -P INPUT DROP
|
||||
iptables -P INPUT ACCEPT # accept by default to allow network drives to persist
|
||||
iptables -P FORWARD ACCEPT # TODO: disable icc and make this as reject
|
||||
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
|
||||
# caas has ssh on port 202
|
||||
if [[ "${PROVIDER}" == "caas" ]]; then
|
||||
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
|
||||
else
|
||||
iptables -A INPUT -p tcp -m tcp -m multiport --dports 80,22,443,886 -j ACCEPT
|
||||
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,22,443,587,993,4190 -j ACCEPT
|
||||
fi
|
||||
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
|
||||
@@ -85,7 +65,7 @@ iptables -A OUTPUT -o lo -j ACCEPT
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -N LOGGING # new chain
|
||||
iptables -A INPUT -j LOGGING # last rule in INPUT chain
|
||||
iptables -A INPUT -j LOGGING # last rule in INPUT chain (log and drop)
|
||||
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -A LOGGING -j DROP
|
||||
|
||||
@@ -94,6 +74,7 @@ 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
|
||||
# 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
|
||||
@@ -119,7 +100,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 --dns-search=.
|
||||
MountFlags=slave
|
||||
LimitNOFILE=1048576
|
||||
LimitNPROC=1048576
|
||||
@@ -130,7 +111,12 @@ WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "=== Setup btrfs data ==="
|
||||
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
|
||||
if ! grep -q loop.ko /lib/modules/`uname -r`/modules.builtin; then
|
||||
# on scaleway loop is not built-in
|
||||
echo "loop" >> /etc/modules
|
||||
modprobe loop
|
||||
fi
|
||||
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by cloudron-system-setup.service)
|
||||
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
|
||||
mkdir -p "${USER_DATA_DIR}"
|
||||
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
|
||||
@@ -150,40 +136,41 @@ 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
|
||||
update-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
|
||||
update-grub
|
||||
elif [[ "${PROVIDER}" == "ec2" ]] || [[ "${PROVIDER}" == "generic" ]]; then
|
||||
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
fi
|
||||
|
||||
# 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
|
||||
@@ -191,11 +178,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
|
||||
@@ -210,42 +197,16 @@ 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
|
||||
echo "=== Prepare installer revision - ${INSTALLER_REVISION}) ==="
|
||||
rm -rf /tmp/box && mkdir -p /tmp/box
|
||||
curl "https://git.cloudron.io/cloudron/box/repository/archive.tar.gz?ref=${INSTALLER_REVISION}" | tar zxvf - --strip-components=1 -C /tmp/box
|
||||
mkdir -p "${INSTALLER_SOURCE_DIR}"
|
||||
cp -rf /tmp/box/installer/* "${INSTALLER_SOURCE_DIR}" && rm -rf /tmp/box
|
||||
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
|
||||
echo "${INSTALLER_REVISION}" > "${INSTALLER_SOURCE_DIR}/REVISION"
|
||||
|
||||
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
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
|
||||
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
|
||||
; kill any child (installer.sh) as well
|
||||
KillMode=control-group
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
echo "==== Install cloudron-version tool ===="
|
||||
npm install -g cloudron-version@0.1.1
|
||||
|
||||
# Restore iptables before docker
|
||||
echo "==== Install iptables-restore systemd script ===="
|
||||
@@ -265,16 +226,17 @@ EOF
|
||||
|
||||
# Allocate swap files
|
||||
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
|
||||
echo "==== Install box-setup systemd script ===="
|
||||
cat > /etc/systemd/system/box-setup.service <<EOF
|
||||
# On ubuntu ec2 we use cloud-init https://wiki.archlinux.org/index.php/Cloud-init
|
||||
echo "==== Install cloudron-system-setup systemd script ===="
|
||||
cat > /etc/systemd/system/cloudron-system-setup.service <<EOF
|
||||
[Unit]
|
||||
Description=Box Setup
|
||||
Before=docker.service collectd.service mysql.service
|
||||
After=do-resize.service
|
||||
Before=docker.service collectd.service mysql.service sshd.service nginx.service
|
||||
After=cloud-init.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/box-setup.sh"
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/cloudron-system-setup.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
@@ -282,9 +244,8 @@ WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable cloudron-installer
|
||||
systemctl enable iptables-restore
|
||||
systemctl enable box-setup
|
||||
systemctl enable cloudron-system-setup
|
||||
|
||||
# Configure systemd
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
@@ -311,14 +272,21 @@ 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
|
||||
# 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
|
||||
|
||||
echo "==== Install ssh ==="
|
||||
apt-get -y install openssh-server
|
||||
|
||||
# caas has ssh on port 202 and we disable password login
|
||||
if [[ "${PROVIDER}" == "caas" ]]; then
|
||||
# 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' \
|
||||
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
|
||||
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
|
||||
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
|
||||
-e 's/^#\?Port .*/Port 202/g' \
|
||||
-i /etc/ssh/sshd_config
|
||||
|
||||
# required so we can connect to this machine since port 22 is blocked by iptables by now
|
||||
|
||||
@@ -14,7 +14,6 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
oauthproxy = require('./src/oauthproxy.js'),
|
||||
server = require('./src/server.js'),
|
||||
simpleauth = require('./src/simpleauth.js');
|
||||
|
||||
@@ -37,12 +36,12 @@ async.series([
|
||||
ldap.start,
|
||||
simpleauth.start,
|
||||
appHealthMonitor.start,
|
||||
oauthproxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Cloudron is up and running');
|
||||
});
|
||||
|
||||
var NOOP_CALLBACK = function () { };
|
||||
@@ -51,7 +50,6 @@ process.on('SIGINT', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
simpleauth.stop(NOOP_CALLBACK);
|
||||
oauthproxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -59,6 +57,5 @@ process.on('SIGTERM', function () {
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
simpleauth.stop(NOOP_CALLBACK);
|
||||
oauthproxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
./node_modules/.bin/apidoc -i src/routes -o docs
|
||||
@@ -0,0 +1,384 @@
|
||||
# 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"
|
||||
```
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
```
|
||||
[](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.
|
||||
|
||||
[](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
|
||||
|
||||
[](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
|
||||
|
||||
[](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
|
||||
|
||||
or as SVG
|
||||
|
||||
[](https://cloudron.io/button.html?app=io.gogs.cloudronapp)
|
||||
|
||||
_Note_: Clicking the buttons above will install [Gogs](http://gogs.io/) on your Cloudron.
|
||||
@@ -0,0 +1,468 @@
|
||||
# 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": [ "https://images.rapgenius.com/fd0175ef780e2feefb30055be9f2e022.520x343x1.jpg" ]
|
||||
}
|
||||
```
|
||||
|
||||
# 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 (required for submitting to the Cloudron Store)
|
||||
|
||||
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 (required for submitting to the Cloudron Store)
|
||||
|
||||
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 (required for submitting to the Cloudron Store)
|
||||
|
||||
The `mediaLinks` field contains an array of links that the Cloudron Store uses to display a slide show of pictures of the application.
|
||||
|
||||
They have to be publicly reachable via `https` and should have an aspect ratio of 3 to 1.
|
||||
For example `600px by 200px` (with/height).
|
||||
|
||||
```
|
||||
"mediaLinks": [
|
||||
"https://s3.amazonaws.com/cloudron-app-screenshots/org.owncloud.cloudronapp/556f6a1d82d5e27a7c4fca427ebe6386d373304f/2.jpg",
|
||||
"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.
|
||||
|
||||
## optionalSso
|
||||
|
||||
Type: boolean
|
||||
|
||||
Required: no
|
||||
|
||||
The `optionalSso` field can be set to true for apps that can be installed optionally without using the Cloudron user management.
|
||||
|
||||
This only applies if any Cloudron auth related addons are used. When set, the Cloudron will not inject the auth related addon environment variables.
|
||||
Any app startup scripts have to be able to deal with missing env variables in this case.
|
||||
|
||||
## tagline
|
||||
|
||||
Type: one-line string
|
||||
|
||||
Required: no (required for submitting to the Cloudron Store)
|
||||
|
||||
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 (required for submitting to the Cloudron Store)
|
||||
|
||||
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"
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,380 @@
|
||||
# Overview
|
||||
|
||||
The Cloudron platform can be installed on public cloud servers from EC2, Digital Ocean, Hetzner,
|
||||
Linode, OVH, Scaleway, Vultr etc. Running Cloudron on a home server or company intranet is work
|
||||
in progress.
|
||||
|
||||
If you run into any trouble following this guide, ask us at our [chat](https://chat.cloudron.io).
|
||||
|
||||
# Understand
|
||||
|
||||
Before installing the Cloudron, it is helpful to understand Cloudron's design. The Cloudron
|
||||
intends to make self-hosting effortless. It takes care of updates, backups, firewall, dns setup,
|
||||
certificate management etc. All app and user configuration is carried out using the web interface.
|
||||
|
||||
This approach to self-hosting means that the Cloudron takes complete ownership of the server and
|
||||
only tracks changes that were made via the web interface. Any external changes made to the server
|
||||
(i.e other than via the Cloudron web interface or API) may be lost across updates.
|
||||
|
||||
The Cloudron requires a domain name when it is installed. Apps are installed into subdomains.
|
||||
The `my` subdomain is special and is the location of the Cloudron web interface. For this to
|
||||
work, the Cloudron requires a way to programmatically configure the DNS entries of the domain.
|
||||
Note that the Cloudron will never overwrite _existing_ DNS entries and refuse to install
|
||||
apps on existing subdomains.
|
||||
|
||||
# CLI Tool
|
||||
|
||||
The [Cloudron tool](https://git.cloudron.io/cloudron/cloudron-cli) is useful for managing
|
||||
a Cloudron. <b class="text-danger">The Cloudron CLI tool has to be run on a Laptop or PC</b>
|
||||
|
||||
## 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. Please contact us on our [chat](https://chat.cloudron.io) if you want to help with Windows support.
|
||||
|
||||
# Provider
|
||||
|
||||
DigitalOcean and EC2 (Amazon Web Services) are frequently tested by us.
|
||||
|
||||
Please use the below links to support us with referrals:
|
||||
* [Amazon EC2](https://aws.amazon.com/ec2/)
|
||||
* [DigitalOcean](https://m.do.co/c/933831d60a1e)
|
||||
|
||||
In addition to those, the Cloudron community has successfully installed the platform on those providers:
|
||||
* [Amazon Lightsail](https://amazonlightsail.com/)
|
||||
* [hosttech](https://www.hosttech.ch/)
|
||||
* [Linode](https://www.linode.com/)
|
||||
* [OVH](https://www.ovh.com/)
|
||||
* [Scaleway](https://www.scaleway.com/)
|
||||
* [So you Start](https://www.soyoustart.com/)
|
||||
* [Vultr](http://www.vultr.com/?ref=7063201)
|
||||
|
||||
Please let us know if any of them requires tweaks or adjustments.
|
||||
|
||||
# Installing
|
||||
|
||||
## Choose Domain
|
||||
|
||||
A domain name is required when installing the Cloudron. Currently, 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.
|
||||
|
||||
The domain name must use one of the following name servers:
|
||||
* AWS Route 53
|
||||
* Digital Ocean
|
||||
* Wildcard - If your domain does not use any of the name servers above, you can manually add
|
||||
a wildcard (`*`) DNS entry.
|
||||
|
||||
You will have to provide the DNS API credentials after you complete the installation.
|
||||
|
||||
## Create server
|
||||
|
||||
Create an `Ubuntu 16.04 (Xenial)` server with at-least `1gb` RAM. Do not make any changes
|
||||
to vanilla ubuntu. Be sure to allocate a static IPv4 address for your server.
|
||||
|
||||
### Linode
|
||||
|
||||
Since Linode does not manage SSH keys, be sure to add the public key to
|
||||
`/root/.ssh/authorized_keys`.
|
||||
|
||||
### Scaleway
|
||||
|
||||
Use the [boot script](https://github.com/scaleway-community/scaleway-docker/issues/2) to
|
||||
enable memory accouting.
|
||||
|
||||
## Setup `my` subdomain
|
||||
|
||||
The Cloudron web interface is installed at the `my` subdomain of your domain.
|
||||
Add a `A` DNS record for the `my` subdomain with the IP of the server created
|
||||
above. Doing this will allow the Cloudron to start up with a valid TLS certificate.
|
||||
|
||||
## Run setup
|
||||
|
||||
SSH into your server and run the following commands:
|
||||
|
||||
```
|
||||
wget https://git.cloudron.io/cloudron/box/raw/3d4e3638/scripts/cloudron-setup
|
||||
chmod +x cloudron-setup
|
||||
./cloudron-setup --domain <domain> --provider <digitalocean|ec2|generic|scaleway>
|
||||
```
|
||||
|
||||
The setup will take around 10-15 minutes.
|
||||
|
||||
`cloudron-setup` takes the following arguments:
|
||||
|
||||
* `--domain` is the domain name in which apps are installed. Currently, only Second Level
|
||||
Domains are supported. For example, `example.com`, `example.co.uk`, `example.rocks` will
|
||||
work fine. Choosing a domain name at any other level like `cloudron.example.com` will not
|
||||
work.
|
||||
|
||||
* `--provider` is the name of your VPS provider. If the name is not on the list, simply
|
||||
choose `generic`. In most cases, the `generic` provider mostly will work fine.
|
||||
If the Cloudron does not complete initialization, it may mean that
|
||||
we have to add some vendor specific quirks. Please open a
|
||||
[bug report](https://git.cloudron.io/cloudron/box/issues) in that case.
|
||||
|
||||
Optional arguments for installation:
|
||||
|
||||
* `--tls-provider` is the name of the SSL/TLS certificate backend. Defaults to Let's encrypt.
|
||||
Specifying `fallback` will setup the Cloudron to use the fallback wildcard certificate.
|
||||
Initially a self-signed one is provided, which can be overwritten later in the admin interface.
|
||||
This may be useful for non-public installations.
|
||||
|
||||
Optional arguments used for update and restore:
|
||||
|
||||
* `--version` is the version of Cloudron to install. By default, the setup script installs
|
||||
the latest version. You can set this to an older version when restoring a Cloudron from a backup.
|
||||
|
||||
* `--restore-url` is a backup URL to restore from.
|
||||
|
||||
## Finish setup
|
||||
|
||||
Once the setup script completes, the server will reboot, then visit `https://my.<domain>` to complete the installation.
|
||||
|
||||
Please note the following:
|
||||
|
||||
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 server and start over.
|
||||
|
||||
Once the setup is done, you can access the admin page in the future at `https://my.<domain>`.
|
||||
|
||||
## DNS
|
||||
|
||||
Cloudron has to be given the API credentials for configuring your domain under `Certs & Domains`
|
||||
in the web UI.
|
||||
|
||||
### Route 53
|
||||
|
||||
Create root or IAM credentials and choose `Route 53` as the DNS provider.
|
||||
|
||||
* 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": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Digital Ocean
|
||||
|
||||
Create an API token with read+write access and choose `Digital Ocean` as the DNS provider.
|
||||
|
||||
### Other
|
||||
|
||||
If your domain *does not* use Route 53 or Digital Ocean, setup a wildcard (`*`) DNS `A` record that points to the
|
||||
IP of the server created above. If your DNS provider has an API, please open an
|
||||
[issue](https://git.cloudron.io/cloudron/box/issues) and we may be able to support it.
|
||||
|
||||
## Backups
|
||||
|
||||
The Cloudron creates encrypted backups once a day. Each app is backed up independently and these
|
||||
backups have the prefix `appbackup_`. The platform state is backed up independently with the
|
||||
prefix `backup_`.
|
||||
|
||||
By default, backups reside in `/var/backups`. Please note that having backups reside in the same
|
||||
physical machine as the Cloudron server instance is dangerous and it must be changed to
|
||||
an external storage location like `S3` as soon as possible.
|
||||
|
||||
### Amazon S3
|
||||
|
||||
Provide S3 backup credentials in the `Settings` page and leave the endpoint field empty.
|
||||
|
||||
Create a bucket in S3 (You have to have an account at [AWS](https://aws.amazon.com/)). The bucket can be setup to periodically delete old backups by
|
||||
adding a lifecycle rule 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 sufficient
|
||||
for most use-cases.
|
||||
|
||||
* 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": "s3:*",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::<your bucket name>",
|
||||
"arn:aws:s3:::<your bucket name>/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Minio S3
|
||||
|
||||
[Minio](https://minio.io/) is a distributed object storage server, providing the same API as Amazon S3.
|
||||
Since Cloudron supports S3, any API compatible solution should be supported as well, if this is not the case, let us know.
|
||||
|
||||
Minio can be setup, by following the [installation instructions](https://docs.minio.io/) on any server, which is reachable by the Cloudron.
|
||||
Do not setup Minio on the same server as the Cloudron, this will inevitably result in data loss, if backups are stored on the same instance.
|
||||
|
||||
Once setup, minio will print the necessary information, like login credentials, region and endpoints in its logs.
|
||||
|
||||
```
|
||||
$ ./minio server ./storage
|
||||
|
||||
Endpoint: http://192.168.10.113:9000 http://127.0.0.1:9000
|
||||
AccessKey: GFAWYNJEY7PUSLTHYHT6
|
||||
SecretKey: /fEWk66E7GsPnzE1gohqKDovaytLcxhr0tNWnv3U
|
||||
Region: us-east-1
|
||||
```
|
||||
|
||||
First create a new bucket for the backups, using the minio commandline tools or the webinterface. The bucket has to have **read and write** permissions.
|
||||
|
||||
The information to be copied to the Cloudron's backup settings form may look similar to:
|
||||
|
||||
<img src="/docs/img/minio_backup_config.png" class="shadow"><br/>
|
||||
|
||||
|
||||
# Email
|
||||
|
||||
Cloudron has a built-in email server. By default, it only sends out email on behalf of apps
|
||||
(for example, password reset or notification). You can enable the email server for sending
|
||||
and receiving mail on the `settings` page. This feature is only available if you have setup
|
||||
a DNS provider like Digital Ocean or Route53.
|
||||
|
||||
Your server's IP plays a big role in how emails from our Cloudron get handled. Spammers
|
||||
frequently abuse 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, DMARC automatically and
|
||||
reputation should be easy to get back.
|
||||
|
||||
## Checklist
|
||||
|
||||
* Once your Cloudron is ready, setup a Reverse DNS PTR record to be setup for the `my` subdomain.
|
||||
|
||||
* AWS/EC2 - Fill the PTR [request form](https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/ec2-email-limit-rdns-request.
|
||||
|
||||
* Digital Ocean - Digital Ocean sets up a PTR record based on the droplet's name. So, simply rename
|
||||
your droplet to `my.<domain>`.
|
||||
|
||||
* Scaleway - Edit your security group to allow email. You can also set a PTR record on the interface with your
|
||||
`my.<domain>`.
|
||||
|
||||
* 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/). The Cloudron
|
||||
should get 100%, if not please let us know.
|
||||
|
||||
# Updates
|
||||
|
||||
Apps installed from the Cloudron Store are automatically updated every night.
|
||||
|
||||
The Cloudron platform itself updates in two ways: update or upgrade.
|
||||
|
||||
### Update
|
||||
|
||||
An **update** is applied onto the running server instance. Such updates are performed
|
||||
every night. You can also use the Cloudron UI to initiate an update immediately.
|
||||
|
||||
The Cloudron will always make a complete backup before attempting an update. In the unlikely
|
||||
case an update fails, it can be [restored](/references/selfhosting.html#restore).
|
||||
|
||||
### Upgrade
|
||||
|
||||
An **upgrade** requires a new OS image and thus involves creating the Cloudron from scratch.
|
||||
This process involves creating a new server with the latest code and restoring it from the
|
||||
last backup. Currently only Cloudrons using the **S3 backup storage** support upgrades.
|
||||
Read more about [backup storage](#s3), otherwise contact us in our [chat](https://chat.cloudron.io).
|
||||
|
||||
To upgrade follow these steps closely:
|
||||
|
||||
* Create a new backup - `cloudron machine backup create <domain>`
|
||||
|
||||
* List the latest backup - `cloudron machine backup list <domain>`
|
||||
|
||||
* Make the latest box backup (files starting with `backup_`) public. This can be done from the AWS S3 console as seen here:
|
||||
|
||||
<img src="/docs/img/aws_backup_public.png" class="shadow haze"><br/>
|
||||
|
||||
* Copy the new public URL of the latest backup for use as the `--restore-url` below.
|
||||
|
||||
<img src="/docs/img/aws_backup_link.png" class="shadow haze"><br/>
|
||||
|
||||
* Create a new Cloudron by following the [installing](/references/selfhosting.html#installing) section.
|
||||
When running the setup script, pass in the `--encryption-key` and `--restore-url` flags.
|
||||
The `--encryption-key` is the backup encryption key. It can be displayed with `cloudron machine info`
|
||||
|
||||
Similar to the initial installation, a Cloudron upgrade looks like:
|
||||
```
|
||||
$ ssh root@newserverip
|
||||
> wget https://git.cloudron.io/cloudron/box/raw/3d4e3638/scripts/cloudron-setup
|
||||
> chmod +x cloudron-setup
|
||||
> ./cloudron-setup --domain <domain> --provider <digitalocean|ec2|generic|scaleway> --encryption-key <key> --restore-url <publicS3Url>
|
||||
```
|
||||
|
||||
* Finally, once you see the newest version being displayed in your Cloudron webinterface, you can safely delete the old server instance.
|
||||
|
||||
# Restore
|
||||
|
||||
To restore a Cloudron from a specific backup:
|
||||
|
||||
* Select the backup - `cloudron machine backup list <domain>`
|
||||
|
||||
* Make the box backup public (this can be done from the S3 console). Also, copy the URL of
|
||||
the backup for use as the `restore-url` below.
|
||||
|
||||
* Create a new Cloudron by following the [installing](/references/selfhosting.html#installing) section.
|
||||
When running the setup script, pass in the `version`, `restore-key` and `restore-url` flags.
|
||||
The `version` field is the version of the Cloudron that the backup corresponds to (it is embedded
|
||||
in the backup file name).
|
||||
|
||||
* Make the box backup private, once the upgrade is complete.
|
||||
|
||||
# Debug
|
||||
|
||||
You can SSH into your Cloudron and collect logs:
|
||||
|
||||
* `journalctl -a -u box` 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 container's log output, `journalctl -a CONTAINER_ID=<container_id>`.
|
||||
|
||||
# Help
|
||||
|
||||
If you run into any problems, join us at our [chat](https://chat.cloudron.io) or [email us](mailto:support@cloudron.io).
|
||||
@@ -0,0 +1,354 @@
|
||||
# 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
|
||||
|
||||
<i>If you self-host, please refer to the [self-hosting documentation](/references/selfhosting.html#backups) for backups.</i>
|
||||
|
||||
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 group users. It's purpose is two-fold:
|
||||
|
||||
* You can assign one or more groups to apps to restrict who can access for an app.
|
||||
* Each group is a mailing list (forwarding address) constituting of it's members.
|
||||
|
||||
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">
|
||||
|
||||
You can now send mails to `groupname@<domain>` to address all the group members.
|
||||
|
||||
# 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.
|
||||
|
||||
## Enabling Email
|
||||
|
||||
By default, Cloudron's email server only allows apps to send email. To enable users to send and receive email,
|
||||
turn on the option under `Settings`. Turning on this option also allows apps to _receive_ email.
|
||||
|
||||
Once email is enabled, the Cloudron will keep the the `MX` DNS record updated.
|
||||
|
||||
<img src="/docs/img/enable_email.png" class="shadow">
|
||||
|
||||
## Receiving email using 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 using 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 using 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.
|
||||
|
||||
## Forwarding addresses
|
||||
|
||||
Each group on the Cloudron is also a forwarding address. Mails can be addressed to `group@<domain>` and the mail will
|
||||
be sent to each user who is part of the group.
|
||||
|
||||
## Marking Spam
|
||||
|
||||
The spam detection agent on the Cloudron requires training to identify spam. To do this, simply move your junk mails
|
||||
to a pre-created folder named `Spam`. Most mail clients have a Junk or Spam button which does this automatically.
|
||||
|
||||
# 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).
|
||||
@@ -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)
|
||||
@@ -0,0 +1,492 @@
|
||||
# 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
|
||||
|
||||
## Metadata
|
||||
|
||||
Publishing to the Cloudron Store requires apps to have meta data specified in the `CloudronManifest.json`.
|
||||
|
||||
The `cloudron` tool will notify if any such information is missing, prior to uploading.
|
||||
See more information for each field [here](/references/manifest.html).
|
||||
|
||||
## Upload for 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)
|
||||
@@ -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'
|
||||
@@ -54,7 +55,14 @@ console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log();
|
||||
|
||||
|
||||
gulp.task('js-index', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src([
|
||||
'webadmin/src/js/index.js',
|
||||
'webadmin/src/js/client.js',
|
||||
@@ -65,25 +73,37 @@ gulp.task('js-index', function () {
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('index.js', { newLine: ';' }))
|
||||
.pipe(uglify())
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setup', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setup.js', { newLine: ';' }))
|
||||
.pipe(uglify())
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-update', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/update.js'])
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(uglify())
|
||||
.pipe(uglifyer)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(gulp.dest('webadmin/dist/js'))
|
||||
.pipe(gulp.dest('setup/splash/website/js'));
|
||||
|
||||
@@ -1,164 +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
|
||||
<% if (env === 'prod') { %>
|
||||
readonly api_server_origin="https://api.cloudron.io"
|
||||
readonly web_server_origin="https://cloudron.io"
|
||||
<% } else { %>
|
||||
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
|
||||
readonly web_server_origin="https://<%= env %>.cloudron.io"
|
||||
<% } %>
|
||||
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
|
||||
readonly versions_url="https://s3.amazonaws.com/<%= env %>-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-<%= env %>"
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
# Installer
|
||||
|
||||
This subfolder contains all resources, which persist across a Cloudron update.
|
||||
Only code and assets, which are part of the updater belong here.
|
||||
|
||||
Installer is the name which got inherited from times, where this folder contained
|
||||
much more infrastructure components, like a local webserver to facilitate updates.
|
||||
|
||||
|
||||
## installer.sh
|
||||
|
||||
The main entry point for initial provisioning and also updates (not upgrades).
|
||||
|
||||
It is called from:
|
||||
* cloudron-setup (during initial provisioning, restoring or upgrade)
|
||||
* cloudron.js in the box code (during an update)
|
||||
|
||||
Two arguments need to be supplied in this order:
|
||||
1. The public url to download the box release tarball `--sourcetarballurl`
|
||||
2. JSON object which contains the user-data `--data`
|
||||
|
||||
|
||||
## cloudron-system-setup.sh
|
||||
|
||||
This is the systemd unit file script hook, which persists Cloudron updates.
|
||||
Mostly it revolves around setting up various parts of the filesystem, like btrfs
|
||||
volumes and swap files
|
||||
@@ -1,516 +0,0 @@
|
||||
{
|
||||
"name": "installer",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "1.5.0",
|
||||
"from": "async@>=1.5.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.14.1",
|
||||
"from": "body-parser@>=1.12.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
|
||||
"dependencies": {
|
||||
"bytes": {
|
||||
"version": "2.1.0",
|
||||
"from": "bytes@2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz"
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.1",
|
||||
"from": "content-type@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.0",
|
||||
"from": "depd@>=1.1.0 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.3.1",
|
||||
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.2.1",
|
||||
"from": "statuses@>=1.0.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.12",
|
||||
"from": "iconv-lite@0.4.12",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "5.1.0",
|
||||
"from": "qs@5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz"
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.1.4",
|
||||
"from": "raw-body@>=2.1.4 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.4.tgz",
|
||||
"dependencies": {
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"from": "unpipe@1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.9",
|
||||
"from": "type-is@>=1.6.9 <1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||
"dependencies": {
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"from": "media-typer@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.7 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"connect-lastmile": {
|
||||
"version": "0.0.13",
|
||||
"from": "connect-lastmile@0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.1.3",
|
||||
"from": "debug@>=2.1.0 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.0",
|
||||
"from": "ms@0.7.0",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.2.0",
|
||||
"from": "debug@>=2.1.1 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.1",
|
||||
"from": "ms@0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"express": {
|
||||
"version": "4.13.3",
|
||||
"from": "express@>=4.11.2 <5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
|
||||
"dependencies": {
|
||||
"accepts": {
|
||||
"version": "1.2.13",
|
||||
"from": "accepts@>=1.2.12 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
|
||||
"dependencies": {
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.5.3",
|
||||
"from": "negotiator@0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"from": "array-flatten@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.0",
|
||||
"from": "content-disposition@0.5.0",
|
||||
"resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
|
||||
},
|
||||
"content-type": {
|
||||
"version": "1.0.1",
|
||||
"from": "content-type@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||
},
|
||||
"cookie": {
|
||||
"version": "0.1.3",
|
||||
"from": "cookie@0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
|
||||
},
|
||||
"cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"from": "cookie-signature@1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.0.1",
|
||||
"from": "depd@>=1.0.1 <1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||
},
|
||||
"escape-html": {
|
||||
"version": "1.0.2",
|
||||
"from": "escape-html@1.0.2",
|
||||
"resolved": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
|
||||
},
|
||||
"etag": {
|
||||
"version": "1.7.0",
|
||||
"from": "etag@>=1.7.0 <1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "0.4.0",
|
||||
"from": "finalhandler@0.4.0",
|
||||
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
|
||||
"dependencies": {
|
||||
"unpipe": {
|
||||
"version": "1.0.0",
|
||||
"from": "unpipe@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.3.0",
|
||||
"from": "fresh@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.0",
|
||||
"from": "merge-descriptors@1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.1",
|
||||
"from": "methods@>=1.1.1 <1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.0",
|
||||
"from": "parseurl@>=1.3.0 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"from": "path-to-regexp@0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "1.0.8",
|
||||
"from": "proxy-addr@>=1.0.8 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.8.tgz",
|
||||
"dependencies": {
|
||||
"forwarded": {
|
||||
"version": "0.1.0",
|
||||
"from": "forwarded@>=0.1.0 <0.2.0",
|
||||
"resolved": "http://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
|
||||
},
|
||||
"ipaddr.js": {
|
||||
"version": "1.0.1",
|
||||
"from": "ipaddr.js@1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qs": {
|
||||
"version": "4.0.0",
|
||||
"from": "qs@4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz"
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.0.3",
|
||||
"from": "range-parser@>=1.0.2 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
|
||||
},
|
||||
"send": {
|
||||
"version": "0.13.0",
|
||||
"from": "send@0.13.0",
|
||||
"resolved": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
|
||||
"dependencies": {
|
||||
"destroy": {
|
||||
"version": "1.0.3",
|
||||
"from": "destroy@1.0.3",
|
||||
"resolved": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
|
||||
},
|
||||
"http-errors": {
|
||||
"version": "1.3.1",
|
||||
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.3.4",
|
||||
"from": "mime@1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
|
||||
},
|
||||
"ms": {
|
||||
"version": "0.7.1",
|
||||
"from": "ms@0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||
},
|
||||
"statuses": {
|
||||
"version": "1.2.1",
|
||||
"from": "statuses@>=1.2.1 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"version": "1.10.0",
|
||||
"from": "serve-static@>=1.10.0 <1.11.0",
|
||||
"resolved": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz"
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.9",
|
||||
"from": "type-is@>=1.6.9 <1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||
"dependencies": {
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"from": "media-typer@0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.7",
|
||||
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.19.0",
|
||||
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.0",
|
||||
"from": "utils-merge@1.0.0",
|
||||
"resolved": "http://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.0.1",
|
||||
"from": "vary@>=1.0.1 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"version": "9.0.3",
|
||||
"from": "json@>=9.0.3 <10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json/-/json-9.0.3.tgz"
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.6.1",
|
||||
"from": "morgan@>=1.5.1 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz",
|
||||
"dependencies": {
|
||||
"basic-auth": {
|
||||
"version": "1.0.3",
|
||||
"from": "basic-auth@>=1.0.3 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.3.tgz"
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.0.1",
|
||||
"from": "depd@>=1.0.1 <1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"dependencies": {
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"from": "ee-first@1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"on-headers": {
|
||||
"version": "1.0.1",
|
||||
"from": "on-headers@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy-middleware": {
|
||||
"version": "0.15.0",
|
||||
"from": "proxy-middleware@>=0.15.0 <0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz"
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.19",
|
||||
"from": "safetydance@0.0.19",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.1.0",
|
||||
"from": "semver@>=5.1.0 <6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
|
||||
},
|
||||
"superagent": {
|
||||
"version": "0.21.0",
|
||||
"from": "superagent@>=0.21.0 <0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-0.21.0.tgz",
|
||||
"dependencies": {
|
||||
"component-emitter": {
|
||||
"version": "1.1.2",
|
||||
"from": "component-emitter@1.1.2",
|
||||
"resolved": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.0.1",
|
||||
"from": "cookiejar@2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz"
|
||||
},
|
||||
"extend": {
|
||||
"version": "1.2.1",
|
||||
"from": "extend@>=1.2.1 <1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
|
||||
},
|
||||
"form-data": {
|
||||
"version": "0.1.3",
|
||||
"from": "form-data@0.1.3",
|
||||
"resolved": "http://registry.npmjs.org/form-data/-/form-data-0.1.3.tgz",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.9.2",
|
||||
"from": "async@>=0.9.0 <0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "0.0.7",
|
||||
"from": "combined-stream@>=0.0.4 <0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
|
||||
"dependencies": {
|
||||
"delayed-stream": {
|
||||
"version": "0.0.5",
|
||||
"from": "delayed-stream@0.0.5",
|
||||
"resolved": "http://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "1.0.14",
|
||||
"from": "formidable@1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.0.1",
|
||||
"from": "methods@1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz"
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.2.11",
|
||||
"from": "mime@1.2.11",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
|
||||
},
|
||||
"qs": {
|
||||
"version": "1.2.0",
|
||||
"from": "qs@1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz"
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "1.0.27-1",
|
||||
"from": "readable-stream@1.0.27-1",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz",
|
||||
"dependencies": {
|
||||
"core-util-is": {
|
||||
"version": "1.0.1",
|
||||
"from": "core-util-is@>=1.0.0 <1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.1",
|
||||
"from": "inherits@>=2.0.1 <2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"from": "isarray@0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"from": "string_decoder@>=0.10.0 <0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reduce-component": {
|
||||
"version": "1.0.1",
|
||||
"from": "reduce-component@1.0.1",
|
||||
"resolved": "http://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "installer",
|
||||
"description": "Cloudron Installer",
|
||||
"version": "0.0.1",
|
||||
"private": "true",
|
||||
"author": {
|
||||
"name": "Cloudron authors"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git"
|
||||
},
|
||||
"engines": [
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.5.0",
|
||||
"body-parser": "^1.12.0",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"debug": "^2.1.1",
|
||||
"express": "^4.11.2",
|
||||
"json": "^9.0.3",
|
||||
"morgan": "^1.5.1",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"safetydance": "0.0.19",
|
||||
"semver": "^5.1.0",
|
||||
"superagent": "^0.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"colors": "^1.1.2",
|
||||
"commander": "^2.8.1",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^0.3.5",
|
||||
"lodash": "^3.2.0",
|
||||
"mocha": "^2.1.0",
|
||||
"nock": "^0.59.1",
|
||||
"sleep": "^3.0.0",
|
||||
"superagent-sync": "^0.2.0",
|
||||
"supererror": "^0.7.0",
|
||||
"yesno": "0.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
"postmerge": "/bin/true"
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,20 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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 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
|
||||
@@ -17,14 +23,16 @@ chmod +x /home/yellowtent/provision.sh
|
||||
|
||||
arg_source_tarball_url=""
|
||||
arg_data=""
|
||||
arg_data_file=""
|
||||
|
||||
args=$(getopt -o "" -l "sourcetarballurl:,data:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "sourcetarballurl:,data:,data-file:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--sourcetarballurl) arg_source_tarball_url="$2";;
|
||||
--data) arg_data="$2";;
|
||||
--data-file) arg_data_file="$2";;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -32,27 +40,52 @@ while true; do
|
||||
shift 2
|
||||
done
|
||||
|
||||
if [[ ! -z ${arg_data_file} ]]; then
|
||||
arg_data=$(cat "${arg_data_file}")
|
||||
fi
|
||||
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
echo "Downloading box code from ${arg_source_tarball_url} to ${box_src_tmp_dir}"
|
||||
|
||||
while true; do
|
||||
for try in `seq 1 10`; do
|
||||
if $curl -L "${arg_source_tarball_url}" | tar -zxf - -C "${box_src_tmp_dir}"; then break; fi
|
||||
echo "Failed to download source tarball, trying again"
|
||||
sleep 5
|
||||
done
|
||||
while true; do
|
||||
|
||||
if [[ ${try} -eq 10 ]]; then
|
||||
echo "Release tarball download failed"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# ensure ownership baked into the tarball is overwritten
|
||||
chown -R root.root "${box_src_tmp_dir}"
|
||||
|
||||
for try in `seq 1 10`; do
|
||||
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||
if cd "${box_src_tmp_dir}" && npm rebuild; then break; fi
|
||||
|
||||
# We need --unsafe-perm as we run as root and the folder is owned by root,
|
||||
# however by default npm drops privileges for npm rebuild
|
||||
# https://docs.npmjs.com/misc/config#unsafe-perm
|
||||
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
|
||||
echo "Failed to rebuild, trying again"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 10 ]]; then
|
||||
echo "npm rebuild failed"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "Setting up update splash screen"
|
||||
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" # show splash from new code
|
||||
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
|
||||
fi
|
||||
|
||||
# ensure we are not inside the source directory, which we will remove now
|
||||
cd /root
|
||||
|
||||
# switch the codes
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
@@ -1,112 +0,0 @@
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
child_process = require('child_process'),
|
||||
debug = require('debug')('installer:installer'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
exports = module.exports = {
|
||||
InstallerError: InstallerError,
|
||||
|
||||
provision: provision,
|
||||
|
||||
_ensureVersion: ensureVersion
|
||||
};
|
||||
|
||||
var INSTALLER_CMD = path.join(__dirname, 'scripts/installer.sh'),
|
||||
SUDO = '/usr/bin/sudo';
|
||||
|
||||
function InstallerError(reason, info) {
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
this.message = !info ? reason : (typeof info === 'object' ? JSON.stringify(info) : info);
|
||||
}
|
||||
util.inherits(InstallerError, Error);
|
||||
InstallerError.INTERNAL_ERROR = 1;
|
||||
InstallerError.ALREADY_PROVISIONED = 2;
|
||||
|
||||
// system until file has KillMode=control-group to bring down child processes
|
||||
function spawn(tag, cmd, args, callback) {
|
||||
assert.strictEqual(typeof tag, 'string');
|
||||
assert.strictEqual(typeof cmd, 'string');
|
||||
assert(util.isArray(args));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cp = child_process.spawn(cmd, args, { timeout: 0 });
|
||||
cp.stdout.setEncoding('utf8');
|
||||
cp.stdout.on('data', function (data) { debug('%s (stdout): %s', tag, data); });
|
||||
cp.stderr.setEncoding('utf8');
|
||||
cp.stderr.on('data', function (data) { debug('%s (stderr): %s', tag, data); });
|
||||
|
||||
cp.on('error', function (error) {
|
||||
debug('%s : child process errored %s', tag, error.message);
|
||||
callback(error);
|
||||
});
|
||||
|
||||
cp.on('exit', function (code, signal) {
|
||||
debug('%s : child process exited. code: %d signal: %d', tag, code, signal);
|
||||
if (signal) return callback(new Error('Exited with signal ' + signal));
|
||||
if (code !== 0) return callback(new Error('Exited with code ' + code));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureVersion(args, callback) {
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!args.data || !args.data.boxVersionsUrl) return callback(new Error('No boxVersionsUrl specified'));
|
||||
|
||||
if (args.sourceTarballUrl) return callback(null, args);
|
||||
|
||||
superagent.get(args.data.boxVersionsUrl).end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var versions = safe.JSON.parse(result.text);
|
||||
|
||||
if (!versions || typeof versions !== 'object') return callback(new Error('versions is not in valid format:' + safe.error));
|
||||
|
||||
var latestVersion = Object.keys(versions).sort(semver.compare).pop();
|
||||
debug('ensureVersion: Latest version is %s etag:%s', latestVersion, result.header['etag']);
|
||||
|
||||
if (!versions[latestVersion]) return callback(new Error('No version available'));
|
||||
if (!versions[latestVersion].sourceTarballUrl) return callback(new Error('No sourceTarballUrl specified'));
|
||||
|
||||
args.sourceTarballUrl = versions[latestVersion].sourceTarballUrl;
|
||||
args.data.version = latestVersion;
|
||||
|
||||
callback(null, args);
|
||||
});
|
||||
}
|
||||
|
||||
function provision(args, callback) {
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (process.env.NODE_ENV === 'test') return callback(null);
|
||||
|
||||
ensureVersion(args, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var pargs = [ INSTALLER_CMD ];
|
||||
pargs.push('--sourcetarballurl', result.sourceTarballUrl);
|
||||
pargs.push('--data', JSON.stringify(result.data));
|
||||
|
||||
debug('provision: calling with args %j', pargs);
|
||||
|
||||
// sudo is required for update()
|
||||
spawn('provision', SUDO, pargs, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* jslint node: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
debug = require('debug')('installer:server'),
|
||||
express = require('express'),
|
||||
fs = require('fs'),
|
||||
http = require('http'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||
installer = require('./installer.js'),
|
||||
json = require('body-parser').json,
|
||||
lastMile = require('connect-lastmile'),
|
||||
morgan = require('morgan'),
|
||||
superagent = require('superagent');
|
||||
|
||||
exports = module.exports = {
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
|
||||
var PROVISION_CONFIG_FILE = '/root/provision.json';
|
||||
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);
|
||||
|
||||
installer.provision(userData, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function provisionLocal(callback) {
|
||||
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
|
||||
|
||||
if (!fs.existsSync(PROVISION_CONFIG_FILE)) {
|
||||
console.error('No provisioning data found at %s', PROVISION_CONFIG_FILE);
|
||||
return callback(new Error('No provisioning data found'));
|
||||
}
|
||||
|
||||
var userData = require(PROVISION_CONFIG_FILE);
|
||||
|
||||
installer.provision(userData, callback);
|
||||
}
|
||||
|
||||
function update(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (!req.body.sourceTarballUrl || typeof req.body.sourceTarballUrl !== 'string') return next(new HttpError(400, 'No sourceTarballUrl provided'));
|
||||
if (!req.body.data || typeof req.body.data !== 'object') return next(new HttpError(400, 'No data provided'));
|
||||
|
||||
debug('provision: received from box %j', req.body);
|
||||
|
||||
installer.provision(req.body, function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
|
||||
next(new HttpSuccess(202, { }));
|
||||
}
|
||||
|
||||
function startUpdateServer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Starting update server');
|
||||
|
||||
var app = express();
|
||||
|
||||
var router = new express.Router();
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev', { immediate: false }));
|
||||
|
||||
app.use(json({ strict: true }))
|
||||
.use(router)
|
||||
.use(lastMile());
|
||||
|
||||
router.post('/api/v1/installer/update', update);
|
||||
|
||||
gHttpServer = http.createServer(app);
|
||||
gHttpServer.on('error', console.error);
|
||||
|
||||
gHttpServer.listen(2020, '127.0.0.1', callback);
|
||||
}
|
||||
|
||||
function stopUpdateServer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Stopping update server');
|
||||
|
||||
if (!gHttpServer) return callback(null);
|
||||
|
||||
gHttpServer.close(callback);
|
||||
gHttpServer = null;
|
||||
}
|
||||
|
||||
function start(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var actions;
|
||||
|
||||
if (process.env.PROVISION === 'local') {
|
||||
debug('Starting Installer in selfhost mode');
|
||||
|
||||
actions = [
|
||||
startUpdateServer,
|
||||
provisionLocal
|
||||
];
|
||||
} else { // current fallback, should be 'digitalocean' eventually, see initializeBaseUbuntuImage.sh
|
||||
debug('Starting Installer in managed mode');
|
||||
|
||||
actions = [
|
||||
startUpdateServer,
|
||||
provisionDigitalOcean
|
||||
];
|
||||
}
|
||||
|
||||
async.series(actions, callback);
|
||||
}
|
||||
|
||||
function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
stopUpdateServer
|
||||
], callback);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
start(function (error) {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
var expect = require('expect.js'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
nock = require('nock'),
|
||||
os = require('os'),
|
||||
request = require('superagent'),
|
||||
server = require('../server.js'),
|
||||
installer = require('../installer.js'),
|
||||
_ = require('lodash');
|
||||
|
||||
var EXTERNAL_SERVER_URL = 'https://localhost:4443';
|
||||
var INTERNAL_SERVER_URL = 'http://localhost:2020';
|
||||
var APPSERVER_ORIGIN = 'http://appserver';
|
||||
var FQDN = os.hostname();
|
||||
|
||||
describe('Server', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
before(function (done) {
|
||||
var user_data = JSON.stringify({ apiServerOrigin: APPSERVER_ORIGIN }); // user_data is a string
|
||||
var scope = nock('http://169.254.169.254')
|
||||
.persist()
|
||||
.get('/metadata/v1.json')
|
||||
.reply(200, JSON.stringify({ user_data: user_data }), { 'Content-Type': 'application/json' });
|
||||
done();
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
nock.cleanAll();
|
||||
done();
|
||||
});
|
||||
|
||||
describe('starts and stop', function () {
|
||||
it('starts', function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
it('stops', function (done) {
|
||||
server.stop(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update (internal server)', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
after(function (done) {
|
||||
server.stop(done);
|
||||
});
|
||||
|
||||
it('does not respond to provision', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/provision').send({ }).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not respond to restore', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/restore').send({ }).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(404);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var data = {
|
||||
sourceTarballUrl: "https://foo.tar.gz",
|
||||
|
||||
data: {
|
||||
token: 'sometoken',
|
||||
apiServerOrigin: APPSERVER_ORIGIN,
|
||||
webServerOrigin: 'https://somethingelse.com',
|
||||
fqdn: 'www.something.com',
|
||||
tlsKey: 'key',
|
||||
tlsCert: 'cert',
|
||||
boxVersionsUrl: 'https://versions.json',
|
||||
version: '0.1'
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(data).forEach(function (key) {
|
||||
it('fails due to missing ' + key, function (done) {
|
||||
var dataCopy = _.merge({ }, data);
|
||||
delete dataCopy[key];
|
||||
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(dataCopy).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(data).end(function (error, result) {
|
||||
expect(error).to.not.be.ok();
|
||||
expect(result.statusCode).to.equal(202);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureVersion', function () {
|
||||
before(function () {
|
||||
process.env.NODE_ENV = undefined;
|
||||
});
|
||||
|
||||
after(function () {
|
||||
process.env.NODE_ENV = 'test';
|
||||
});
|
||||
|
||||
it ('fails without data', function (done) {
|
||||
installer._ensureVersion({}, function (error) {
|
||||
expect(error).to.be.an(Error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('fails without boxVersionsUrl', function (done) {
|
||||
installer._ensureVersion({ data: {}}, function (error) {
|
||||
expect(error).to.be.an(Error);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with sourceTarballUrl', function (done) {
|
||||
var data = {
|
||||
sourceTarballUrl: 'sometarballurl',
|
||||
data: {
|
||||
boxVersionsUrl: 'http://foobar/versions.json'
|
||||
}
|
||||
};
|
||||
|
||||
installer._ensureVersion(data, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result).to.eql(data);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds without sourceTarballUrl', function (done) {
|
||||
var versions = {
|
||||
'0.1.0': {
|
||||
sourceTarballUrl: 'sometarballurl1'
|
||||
},
|
||||
'0.2.0': {
|
||||
sourceTarballUrl: 'sometarballurl2'
|
||||
}
|
||||
};
|
||||
|
||||
var scope = nock('http://foobar')
|
||||
.get('/versions.json')
|
||||
.reply(200, JSON.stringify(versions), { 'Content-Type': 'application/json' });
|
||||
|
||||
var data = {
|
||||
data: {
|
||||
boxVersionsUrl: 'http://foobar/versions.json'
|
||||
}
|
||||
};
|
||||
|
||||
installer._ensureVersion(data, function (error, result) {
|
||||
expect(error).to.equal(null);
|
||||
expect(result.sourceTarballUrl).to.equal(versions['0.2.0'].sourceTarballUrl);
|
||||
expect(result.data.boxVersionsUrl).to.equal(data.data.boxVersionsUrl);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,26 +4,20 @@ 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"
|
||||
|
||||
# detect device
|
||||
if [[ -b "/dev/vda1" ]]; then
|
||||
disk_device="/dev/vda1"
|
||||
fi
|
||||
# detect device of rootfs (http://forums.fedoraforum.org/showthread.php?t=270316)
|
||||
disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
|
||||
|
||||
if [[ -b "/dev/xvda1" ]]; then
|
||||
disk_device="/dev/xvda1"
|
||||
fi
|
||||
existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/1024 }')
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
|
||||
readonly swap_size=$((${physical_memory} - ${existing_swap})) # 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_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf "%.0f", $3 }')
|
||||
readonly disk_size=$((disk_size_gb * 1024))
|
||||
readonly backup_swap_size=1024
|
||||
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
|
||||
|
||||
@@ -32,9 +26,8 @@ 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
|
||||
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
|
||||
# Allocate swap for general app usage
|
||||
if [[ ! -f "${APPS_SWAP_FILE}" && ${swap_size} -gt 0 ]]; then
|
||||
echo "Creating Apps swap file of size ${swap_size}M"
|
||||
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
|
||||
chmod 600 "${APPS_SWAP_FILE}"
|
||||
@@ -45,17 +38,8 @@ 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}" || 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
|
||||
@@ -63,4 +47,3 @@ umount "${USER_DATA_DIR}" || true
|
||||
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}"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
var dbm = dbm || require('db-migrate');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerId VARCHAR(128)'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
function addGroupMailboxes(done) {
|
||||
console.log('Importing group mailboxes');
|
||||
|
||||
db.all('SELECT id, name FROM groups', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (g, next) {
|
||||
db.runSql('INSERT INTO mailboxes (ownerId, ownerType, name) VALUES (?, ?, ?)', [ g.id, 'group', g.name ], function (error) {
|
||||
if (error) console.error('Error importing group ' + JSON.stringify(g) + error);
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
function addAppMailboxes(done) {
|
||||
console.log('Importing app mail boxes');
|
||||
|
||||
db.all('SELECT id, location, manifestJson FROM apps', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (a, next) {
|
||||
var manifest = JSON.parse(a.manifestJson);
|
||||
if (!manifest.addons['sendmail'] && !manifest.addons['recvmail']) return next();
|
||||
|
||||
var mailboxName = (a.location ? a.location : manifest.title.replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
db.runSql('INSERT INTO mailboxes (ownerId, ownerType, name) VALUES (?, ?, ?)', [ a.id, 'app', mailboxName ], function (error) {
|
||||
if (error) console.error('Error importing app ' + JSON.stringify(a) + error);
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
function setUserMailboxOwnerIds(done) {
|
||||
console.log('Setting owner id of user mailboxes and aliases');
|
||||
|
||||
db.all('SELECT id, username FROM users', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (u, next) {
|
||||
if (!u.username) return next();
|
||||
|
||||
db.runSql('UPDATE mailboxes SET ownerId = ?, ownerType = ? WHERE name = ? OR aliasTarget = ?', [ u.id, 'user', u.username, u.username ], function (error) {
|
||||
if (error) console.error('Error setting ownerid ' + JSON.stringify(u) + error);
|
||||
next();
|
||||
});
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerId VARCHAR(128) NOT NULL'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(128) NOT NULL'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerId', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', 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 ADD COLUMN sso BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN sso', 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 DROP COLUMN oauthProxy', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -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,11 +19,12 @@ 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,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
@@ -37,7 +38,7 @@ CREATE TABLE IF NOT EXISTS tokens(
|
||||
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(
|
||||
@@ -53,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),
|
||||
@@ -61,15 +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,
|
||||
oauthProxy BOOLEAN DEFAULT 0,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
altDomain VARCHAR(256),
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
|
||||
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));
|
||||
|
||||
@@ -84,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(
|
||||
@@ -99,12 +101,33 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
filename VARCHAR(128) NOT NULL, /* s3 url, currently this is also the id */
|
||||
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,
|
||||
configJson TEXT, /* configuration - bucket, prefix, key, provider */
|
||||
|
||||
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,
|
||||
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
|
||||
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
creationTime TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (name));
|
||||
|
||||
@@ -14,13 +14,12 @@
|
||||
],
|
||||
"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.3.0",
|
||||
"checksum": "^0.1.1",
|
||||
"cloudron-manifestformat": "^2.5.1",
|
||||
"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 +27,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",
|
||||
"ldapjs": "^1.0.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",
|
||||
@@ -58,17 +58,17 @@
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"safetydance": "^0.1.1",
|
||||
"semver": "^4.3.6",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"showdown": "^1.4.4",
|
||||
"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": "*",
|
||||
@@ -89,9 +89,8 @@
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"mocha": "*",
|
||||
"nock": "^3.4.0",
|
||||
"nock": "^9.0.2",
|
||||
"node-sass": "^3.0.0-alpha.0",
|
||||
"redis": "^2.4.2",
|
||||
"request": "^2.65.0",
|
||||
"sinon": "^1.12.2",
|
||||
"yargs": "^3.15.0"
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly INSTALLER_REVISION="v0.91.0"
|
||||
readonly INIT_BASESYSTEM_SCRIPT_URL="https://git.cloudron.io/cloudron/box/raw/${INSTALLER_REVISION}/baseimage/initializeBaseUbuntuImage.sh"
|
||||
readonly INSTALLER_SOURCE_DIR="/home/yellowtent/installer"
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
|
||||
domain=""
|
||||
provider=""
|
||||
encryptionKey=""
|
||||
restoreUrl=""
|
||||
dnsProvider="manual"
|
||||
tlsProvider="le-prod"
|
||||
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
|
||||
version="latest"
|
||||
apiServer="https://api.cloudron.io"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,help,provider:,encryption-key:,restore-url:,tls-provider:,version:,versions-url:,api-server:,dns-provider:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--domain) domain="$2"; shift 2;;
|
||||
--help) echo "See https://cloudron.io/references/selfhosting.html on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
--tls-provider) tlsProvider="$2"; shift 2;;
|
||||
--dns-provider) dnsProvider="$2"; shift 2;;
|
||||
--version) version="$2"; shift 2;;
|
||||
--versions-url) versionsUrl="$2"; shift 2;;
|
||||
--api-server) apiServer="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${domain}" ]]; then
|
||||
echo "--domain is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (generic, scaleway, ec2, digitalocean)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "generic" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "digitalocean" \
|
||||
]]; then
|
||||
echo "--provider must be one of: generic, scaleway, ec2, digitalocean"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${dnsProvider}" ]]; then
|
||||
echo "--dns-provider is required (noop, manual)"
|
||||
exit 1
|
||||
elif [[ "${dnsProvider}" != "noop" && "${dnsProvider}" != "manual" ]]; then
|
||||
echo "--dns-provider must be one of : manual, noop"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "##############################################"
|
||||
echo " Cloudron Setup (${version}) "
|
||||
echo "##############################################"
|
||||
echo ""
|
||||
echo "The server will reboot at the end to complete the setup."
|
||||
echo ""
|
||||
echo " Follow setup logs in a second terminal with:"
|
||||
echo " $ tail -f ${LOG_FILE}"
|
||||
echo ""
|
||||
|
||||
echo "=> Update package repositories ..."
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Installing setup dependencies ..."
|
||||
if ! apt-get install curl dnsutils -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl and dnsutils)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Downloading initialization script"
|
||||
if ! curl -s "${INIT_BASESYSTEM_SCRIPT_URL}" > /tmp/initializeBaseUbuntuImage.sh; then
|
||||
echo "Could not download initialization script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Installing base dependencies ... (this takes some time)"
|
||||
if ! /bin/bash /tmp/initializeBaseUbuntuImage.sh "${INSTALLER_REVISION}" "${provider}" &>> "${LOG_FILE}"; then
|
||||
echo "Init script failed. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
rm /tmp/initializeBaseUbuntuImage.sh
|
||||
|
||||
echo "=> Checking version"
|
||||
NPM_BIN=$(npm bin -g 2>/dev/null)
|
||||
if ! version=$(${NPM_BIN}/cloudron-version --out version --versions-url "${versionsUrl}" --version "${version}"); then
|
||||
echo "No such version ${version}"
|
||||
exit 1
|
||||
fi
|
||||
if ! sourceTarballUrl=$(${NPM_BIN}/cloudron-version --out tarballUrl --versions-url "${versionsUrl}" --version "${version}"); then
|
||||
echo "No source code for version ${version}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Run base init service"
|
||||
systemctl start cloudron-system-setup
|
||||
|
||||
if [[ -z "${restoreUrl}" ]]; then
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServer}",
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
},
|
||||
"dnsConfig": {
|
||||
"provider": "${dnsProvider}"
|
||||
},
|
||||
"backupConfig" : {
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
else
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServer}",
|
||||
"restore": {
|
||||
"url": "${restoreUrl}",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
echo "=> Run installer.sh for version ${version} with ${sourceTarballUrl} ... (this takes some time)"
|
||||
if ! ${INSTALLER_SOURCE_DIR}/scripts/installer.sh --sourcetarballurl "${sourceTarballUrl}" --data "${data}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready ... (this takes some time)"
|
||||
while true; do
|
||||
echo -n "."
|
||||
if journalctl -u box -a | grep "platformReady: " >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Rebooting this server now to let bootloader changes take effect."
|
||||
echo ""
|
||||
echo "Visit https://my.${domain} to finish setup"
|
||||
echo ""
|
||||
|
||||
systemctl reboot
|
||||
@@ -11,15 +11,13 @@ assertNotEmpty() {
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:,publish,no-upload" -n "$0" -- "$@")
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:,no-upload" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
readonly RELEASE_TOOL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../release" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
delete_bundle="yes"
|
||||
commitish="HEAD"
|
||||
publish="no"
|
||||
upload="yes"
|
||||
bundle_file=""
|
||||
|
||||
@@ -28,22 +26,13 @@ while true; do
|
||||
--revision) commitish="$2"; shift 2;;
|
||||
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
|
||||
--no-upload) upload="no"; shift;;
|
||||
--publish) publish="yes"; shift;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${upload}" == "no" && "${publish}" == "yes" ]]; then
|
||||
echo "Cannot publish without uploading"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||
|
||||
assertNotEmpty AWS_DEV_ACCESS_KEY
|
||||
assertNotEmpty AWS_DEV_SECRET_KEY
|
||||
|
||||
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||
echo "You have local changes, stash or commit them to proceed"
|
||||
exit 1
|
||||
@@ -103,16 +92,15 @@ rm -rf "${bundle_dir}"
|
||||
|
||||
if [[ "${upload}" == "yes" ]]; then
|
||||
echo "Uploading bundle to S3"
|
||||
|
||||
assertNotEmpty AWS_DEV_ACCESS_KEY
|
||||
assertNotEmpty AWS_DEV_SECRET_KEY
|
||||
|
||||
# That special header is needed to allow access with singed urls created with different aws credentials than the ones the file got uploaded
|
||||
s3cmd --multipart-chunk-size-mb=5 --ssl --acl-public --access_key="${AWS_DEV_ACCESS_KEY}" --secret_key="${AWS_DEV_SECRET_KEY}" --no-mime-magic put "${bundle_file}" "s3://dev-cloudron-releases/box-${version}.tar.gz"
|
||||
|
||||
versions_file_url="https://dev-cloudron-releases.s3.amazonaws.com/box-${version}.tar.gz"
|
||||
echo "The URL for the versions file is: ${versions_file_url}"
|
||||
|
||||
if [[ "${publish}" == "yes" ]]; then
|
||||
echo "Publishing to dev"
|
||||
${RELEASE_TOOL_DIR}/release create --env dev --code "${versions_file_url}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${delete_bundle}" == "no" ]]; then
|
||||
@@ -120,4 +108,3 @@ if [[ "${delete_bundle}" == "no" ]]; then
|
||||
else
|
||||
rm "${bundle_file}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=23
|
||||
|
||||
# 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.10.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
|
||||
@@ -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,26 +22,53 @@ 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)
|
||||
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
|
||||
|
||||
# only update/restore have this valid (but not migrate)
|
||||
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
|
||||
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
|
||||
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
|
||||
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
|
||||
arg_box_versions_url=$(echo "$2" | $json boxVersionsUrl)
|
||||
[[ "${arg_box_versions_url}" == "" ]] && arg_box_versions_url="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
|
||||
|
||||
# TODO check if an where this is used
|
||||
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)
|
||||
|
||||
arg_provider=$(echo "$2" | $json provider)
|
||||
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
|
||||
|
||||
arg_tls_config=$(echo "$2" | $json tlsConfig)
|
||||
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,12 +25,14 @@ 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
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmbackup.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmbackup.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,24 +116,31 @@ 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
|
||||
cp "${DATA_DIR}/box/certs/host.cert" "${DATA_DIR}/nginx/cert/host.cert"
|
||||
cp "${DATA_DIR}/box/certs/host.key" "${DATA_DIR}/nginx/cert/host.key"
|
||||
else
|
||||
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
|
||||
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
|
||||
if [[ -z "${arg_tls_cert}" || -z "${arg_tls_key}" ]]; then
|
||||
echo "Creating fallback certs"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout "${DATA_DIR}/nginx/cert/host.key" -out "${DATA_DIR}/nginx/cert/host.cert" -days 3650 -subj "/CN=${arg_fqdn}" -nodes
|
||||
else
|
||||
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
|
||||
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
|
||||
fi
|
||||
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 +154,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 +207,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
|
||||
|
||||
@@ -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,15 +36,17 @@ 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;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# only serve up the status page if we get proxy gateway errors
|
||||
error_page 502 503 504 @appstatus;
|
||||
location @appstatus {
|
||||
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
error_page 502 503 504 /appstatus.html;
|
||||
location /appstatus.html {
|
||||
internal;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -58,6 +64,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;
|
||||
@@ -74,9 +81,6 @@ server {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
<% } else if ( endpoint === 'oauthproxy' ) { %>
|
||||
proxy_pass http://127.0.0.1:3003;
|
||||
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'splash' ) { %>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 256m \
|
||||
--memory-swap 512m \
|
||||
-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"
|
||||
@@ -7,7 +7,6 @@ exports = module.exports = {
|
||||
restoreAddons: restoreAddons,
|
||||
|
||||
getEnvironment: getEnvironment,
|
||||
getLinksSync: getLinksSync,
|
||||
getBindsSync: getBindsSync,
|
||||
getContainerNamesSync: getContainerNamesSync,
|
||||
|
||||
@@ -19,30 +18,35 @@ 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'),
|
||||
mailboxdb = require('./mailboxdb.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 +83,12 @@ var KNOWN_ADDONS = {
|
||||
backup: backupPostgreSql,
|
||||
restore: restorePostgreSql
|
||||
},
|
||||
recvmail: {
|
||||
setup: setupRecvMail,
|
||||
teardown: teardownRecvMail,
|
||||
backup: NOOP,
|
||||
restore: setupRecvMail
|
||||
},
|
||||
redis: {
|
||||
setup: setupRedis,
|
||||
teardown: teardownRedis,
|
||||
@@ -199,28 +209,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');
|
||||
@@ -266,23 +254,21 @@ function setupOauth(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
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 +286,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);
|
||||
});
|
||||
@@ -312,24 +298,23 @@ function setupSimpleAuth(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
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 +331,59 @@ 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');
|
||||
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
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,19 +406,24 @@ 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
|
||||
debugApp(app, 'Setting up SendMail');
|
||||
|
||||
var env = [
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
|
||||
'MAIL_SMTP_USERNAME=' + username,
|
||||
'MAIL_SMTP_PASSWORD=' + hat(256), // this is ignored
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debugApp(app, 'Setting up sendmail');
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
var env = [
|
||||
"MAIL_SMTP_SERVER=mail",
|
||||
"MAIL_SMTP_PORT=2525",
|
||||
"MAIL_SMTP_USERNAME=" + mailbox.name,
|
||||
"MAIL_SMTP_PASSWORD=" + app.id,
|
||||
"MAIL_FROM=" + mailbox.name + '@' + config.fqdn(),
|
||||
"MAIL_DOMAIN=" + config.fqdn()
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function teardownSendMail(app, options, callback) {
|
||||
@@ -413,6 +436,42 @@ function teardownSendMail(app, options, callback) {
|
||||
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');
|
||||
|
||||
mailboxdb.getByOwnerId(app.id, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
|
||||
var env = [
|
||||
"MAIL_IMAP_SERVER=mail",
|
||||
"MAIL_IMAP_PORT=9993",
|
||||
"MAIL_IMAP_USERNAME=" + mailbox.name,
|
||||
"MAIL_IMAP_PASSWORD=" + app.id,
|
||||
"MAIL_TO=" + mailbox.name + '@' + config.fqdn(),
|
||||
"MAIL_DOMAIN=" + config.fqdn()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail 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');
|
||||
|
||||
debugApp(app, 'Tearing down recvmail');
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
|
||||
}
|
||||
|
||||
function setupMySql(app, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
@@ -420,31 +479,14 @@ function setupMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'Setting up mysql');
|
||||
|
||||
var container = docker.getContainer('mysql');
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -453,24 +495,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', 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -482,15 +514,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', options.multipleDatabases ? 'backup-prefix' : '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) {
|
||||
@@ -504,17 +530,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', options.multipleDatabases ? 'restore-prefix' : '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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -525,31 +542,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -558,24 +558,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -587,19 +577,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);
|
||||
@@ -609,17 +593,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -630,31 +606,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -663,24 +622,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -692,15 +641,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) {
|
||||
@@ -714,53 +657,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');
|
||||
@@ -777,57 +678,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -836,7 +712,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
|
||||
@@ -859,15 +735,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);
|
||||
}
|
||||
|
||||
@@ -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.memoryLimit' ].join(',');
|
||||
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso' ].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;
|
||||
@@ -96,6 +93,11 @@ function postProcess(result) {
|
||||
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.sso = !!result.sso; // make it bool
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -114,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');
|
||||
@@ -177,26 +163,33 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, 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 memoryLimit, 'number');
|
||||
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 sso = 'sso' in data ? data.sso : null;
|
||||
|
||||
var queries = [ ];
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -300,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]));
|
||||
@@ -361,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'));
|
||||
}
|
||||
|
||||
@@ -17,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;
|
||||
@@ -138,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);
|
||||
|
||||
@@ -154,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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,7 +201,7 @@ function stop(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clearTimeout(gRunTimeout);
|
||||
gDockerEventStream.end();
|
||||
if (gDockerEventStream) gDockerEventStream.end();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -14,12 +12,11 @@ exports = module.exports = {
|
||||
_unconfigureNginx: unconfigureNginx,
|
||||
_createVolume: createVolume,
|
||||
_deleteVolume: deleteVolume,
|
||||
_allocateOAuthProxyCredentials: allocateOAuthProxyCredentials,
|
||||
_removeOAuthProxyCredentials: removeOAuthProxyCredentials,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomain: registerSubdomain,
|
||||
_unregisterSubdomain: unregisterSubdomain,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
_waitForDnsPropagation: waitForDnsPropagation,
|
||||
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
@@ -35,16 +32,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 +54,6 @@ var addons = require('./addons.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
@@ -100,9 +96,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);
|
||||
@@ -158,34 +152,6 @@ function deleteVolume(app, callback) {
|
||||
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
|
||||
}
|
||||
|
||||
function allocateOAuthProxyCredentials(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!nginx.requiresOAuthProxy(app)) return callback(null);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
var clientSecret = hat(256);
|
||||
var redirectURI = 'https://' + config.appFqdn(app.location);
|
||||
var scope = 'profile';
|
||||
|
||||
clientdb.add(id, app.id, clientdb.TYPE_PROXY, clientSecret, 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) {
|
||||
debugApp(app, 'Error removing OAuth client id', error);
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function addCollectdProfile(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -231,17 +197,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 +225,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 +253,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,22 +296,21 @@ function waitForDnsPropagation(app, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
function retry(error) {
|
||||
debugApp(app, 'waitForDnsPropagation: ', error);
|
||||
setTimeout(waitForDnsPropagation.bind(null, app, callback), 5000);
|
||||
}
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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);
|
||||
|
||||
if (result !== 'done') return retry(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
|
||||
callback(null);
|
||||
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
function updateApp(app, values, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
@@ -379,7 +356,6 @@ function install(app, callback) {
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
@@ -387,9 +363,6 @@ function install(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '25, Creating OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
@@ -410,9 +383,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 +412,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) {
|
||||
@@ -480,7 +456,6 @@ function restore(app, callback) {
|
||||
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
@@ -488,9 +463,6 @@ function restore(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '50, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '55, Registering subdomain' }), // ip might change during upgrades
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
@@ -501,7 +473,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 +483,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),
|
||||
|
||||
@@ -548,13 +523,9 @@ function configure(app, callback) {
|
||||
if (!app.oldConfig || app.oldConfig.location === app.location) return next();
|
||||
unregisterSubdomain(app, app.oldConfig.location, next);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
|
||||
allocateOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
|
||||
@@ -573,6 +544,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),
|
||||
|
||||
@@ -616,7 +590,6 @@ function update(app, callback) {
|
||||
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();
|
||||
|
||||
@@ -628,11 +601,14 @@ function update(app, callback) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '30, 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);
|
||||
},
|
||||
|
||||
// only delete unused addons after backup
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
@@ -689,9 +665,6 @@ function uninstall(app, callback) {
|
||||
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '70, Remove OAuth credentials' }),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
@@ -767,6 +740,7 @@ 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, 'Internal error. apptask launched with error status.');
|
||||
@@ -783,6 +757,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;
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -10,17 +8,16 @@ 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,
|
||||
crypto = require('crypto'),
|
||||
groups = require('./groups'),
|
||||
passport = require('passport'),
|
||||
tokendb = require('./tokendb'),
|
||||
user = require('./user'),
|
||||
userdb = require('./userdb'),
|
||||
UserError = user.UserError,
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -28,14 +25,14 @@ 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');
|
||||
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
|
||||
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
|
||||
|
||||
callback(null, result);
|
||||
@@ -44,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);
|
||||
@@ -67,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);
|
||||
@@ -85,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);
|
||||
@@ -101,37 +98,12 @@ 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;
|
||||
|
||||
// amend the admin flag
|
||||
groups.isMember(groups.ADMIN_GROUP_ID, user.id, function (error, isAdmin) {
|
||||
if (error) return callback(error);
|
||||
|
||||
user.admin = isAdmin;
|
||||
|
||||
callback(null, user, info);
|
||||
});
|
||||
callback(null, user, info);
|
||||
});
|
||||
});
|
||||
}));
|
||||
@@ -144,4 +116,3 @@ function uninitialize(callback) {
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'filename', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'configJson' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -27,8 +26,6 @@ function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
|
||||
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
|
||||
result.config = safe.JSON.parse(result.configJson);
|
||||
delete result.configJson;
|
||||
}
|
||||
|
||||
function getPaged(page, perPage, callback) {
|
||||
@@ -52,7 +49,7 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND filename LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
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));
|
||||
|
||||
@@ -62,12 +59,12 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function get(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE filename = ? ORDER BY creationTime DESC',
|
||||
[ filename ], function (error, result) {
|
||||
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));
|
||||
|
||||
@@ -79,17 +76,16 @@ function get(filename, callback) {
|
||||
|
||||
function add(backup, callback) {
|
||||
assert(backup && typeof backup === 'object');
|
||||
assert.strictEqual(typeof backup.filename, 'string');
|
||||
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(backup.config && typeof backup.config === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||
|
||||
database.query('INSERT INTO backups (filename, version, type, creationTime, state, dependsOn, configJson) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.filename, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), JSON.stringify(backup.config) ],
|
||||
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));
|
||||
@@ -107,11 +103,11 @@ function clear(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function del(filename, callback) {
|
||||
assert.strictEqual(typeof filename, 'string');
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM backups WHERE filename=?', [ filename ], function (error) {
|
||||
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -3,25 +3,64 @@
|
||||
exports = module.exports = {
|
||||
BackupsError: BackupsError,
|
||||
|
||||
testConfig: testConfig,
|
||||
|
||||
getPaged: getPaged,
|
||||
getByAppIdPaged: getByAppIdPaged,
|
||||
|
||||
getBackupUrl: getBackupUrl,
|
||||
getAppBackupUrl: getAppBackupUrl,
|
||||
getRestoreUrl: getRestoreUrl,
|
||||
getRestoreConfig: getRestoreConfig,
|
||||
|
||||
copyLastBackup: copyLastBackup
|
||||
ensureBackup: ensureBackup,
|
||||
|
||||
backup: backup,
|
||||
backupApp: backupApp,
|
||||
restoreApp: restoreApp,
|
||||
|
||||
backupBoxAndApps: backupBoxAndApps,
|
||||
|
||||
getLocalDownloadPath: getLocalDownloadPath,
|
||||
|
||||
removeBackup: removeBackup
|
||||
};
|
||||
|
||||
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'),
|
||||
filesystem = require('./storage/filesystem.js'),
|
||||
locker = require('./locker.js'),
|
||||
mailer = require('./mailer.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'),
|
||||
SettingsError = require('./settings.js').SettingsError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
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');
|
||||
@@ -44,6 +83,8 @@ function BackupsError(reason, errorOrMessage) {
|
||||
util.inherits(BackupsError, Error);
|
||||
BackupsError.EXTERNAL_ERROR = 'external error';
|
||||
BackupsError.INTERNAL_ERROR = 'internal error';
|
||||
BackupsError.BAD_STATE = 'bad state';
|
||||
BackupsError.NOT_FOUND = 'not found';
|
||||
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
|
||||
|
||||
// choose which storage backend we use for test purpose we use s3
|
||||
@@ -51,10 +92,21 @@ function api(provider) {
|
||||
switch (provider) {
|
||||
case 'caas': return caas;
|
||||
case 's3': return s3;
|
||||
case 'filesystem': return filesystem;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function testConfig(backupConfig, callback) {
|
||||
assert.strictEqual(typeof backupConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var func = api(backupConfig.provider);
|
||||
if (!func) return callback(new SettingsError(SettingsError.BAD_FIELD, 'unkown storage provider'));
|
||||
|
||||
api(backupConfig.provider).testConfig(backupConfig, callback);
|
||||
}
|
||||
|
||||
function getPaged(page, perPage, callback) {
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
@@ -67,11 +119,6 @@ function getPaged(page, perPage, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// this should probably be provider specific
|
||||
function cleanBackupConfig(backupConfig) {
|
||||
return _.pick(backupConfig, 'provider', 'key', 'bucket', 'prefix', 'region');
|
||||
}
|
||||
|
||||
function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
@@ -85,124 +132,71 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getBackupUrl(appBackupIds, callback) {
|
||||
assert(util.isArray(appBackupIds));
|
||||
// backupId is the filename. appbackup_%s_%s-v%s.tar.gz
|
||||
function getRestoreConfig(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
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).getSignedUploadUrl(backupConfig, filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
api(backupConfig.provider).getAppRestoreConfig(backupConfig, backupId, function (error, result) {
|
||||
if (error && error.reason === BackupsError.NOT_FOUND) return callback(error);
|
||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||
|
||||
var obj = {
|
||||
id: filename,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: backupConfig.key
|
||||
};
|
||||
|
||||
debug('getBackupUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
backupdb.add({ filename: filename, creationTime: now, version: config.version(), type: backupdb.BACKUP_TYPE_BOX,
|
||||
dependsOn: appBackupIds, config: cleanBackupConfig(backupConfig) }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
callback(null, result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAppBackupUrl(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var now = new Date();
|
||||
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), app.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, configFilename, function (error, configResult) {
|
||||
if (error) return callback(error);
|
||||
|
||||
api(backupConfig.provider).getSignedUploadUrl(backupConfig, dataFilename, function (error, dataResult) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var obj = {
|
||||
id: dataFilename,
|
||||
url: dataResult.url,
|
||||
configUrl: configResult.url,
|
||||
sessionToken: dataResult.sessionToken, // this token can be used for both config and data upload
|
||||
backupKey: backupConfig.key // only data is encrypted
|
||||
};
|
||||
|
||||
debug('getAppBackupUrl: %j', obj);
|
||||
|
||||
backupdb.add({ filename: dataFilename, creationTime: now, version: app.manifest.version, type: backupdb.BACKUP_TYPE_APP,
|
||||
dependsOn: [ ], config: cleanBackupConfig(backupConfig) }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
|
||||
// backupId is the filename. appbackup_%s_%s-v%s.tar.gz
|
||||
function getRestoreUrl(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getBackupConfig(function (error, apiConfig) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
backupdb.get(backupId, function (error, result) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
api(backupConfig.provider).getRestoreUrl(backupConfig, backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var backupConfig = result.config;
|
||||
api(backupConfig.provider).getSignedDownloadUrl(apiConfig, backupConfig, backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
var obj = {
|
||||
id: backupId,
|
||||
url: result.url,
|
||||
backupKey: backupConfig.key,
|
||||
sha1: result.sha1 || null // not supported by all backends
|
||||
};
|
||||
|
||||
var obj = {
|
||||
id: backupId,
|
||||
url: result.url,
|
||||
sessionToken: result.sessionToken,
|
||||
backupKey: backupConfig.key
|
||||
};
|
||||
debug('getRestoreUrl: id:%s url:%s backupKey:%s sha1:%s', obj.id, obj.url, obj.backupKey, obj.sha1);
|
||||
|
||||
debug('getRestoreUrl: id:%s url:%s sessionToken:%s backupKey:%s', obj.id, obj.url, obj.sessionToken, obj.backupKey);
|
||||
|
||||
callback(null, obj);
|
||||
});
|
||||
callback(null, obj);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyLastBackup(app, callback) {
|
||||
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 toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, (new Date()).toISOString(), app.manifest.version);
|
||||
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', 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));
|
||||
|
||||
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));
|
||||
|
||||
// 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));
|
||||
|
||||
@@ -211,3 +205,306 @@ function copyLastBackup(app, callback) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
assert(util.isArray(appBackupIds));
|
||||
|
||||
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).getBoxBackupDetails(backupConfig, filename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('backupBoxWithAppBackupIds: backup details %j', result);
|
||||
|
||||
shell.sudo('backupBox', [ BACKUP_BOX_CMD ].concat(result.backupScriptArguments), function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('backupBoxWithAppBackupIds: success');
|
||||
|
||||
backupdb.add({ id: filename, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
webhooks.backupDone(filename, null /* app */, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, filename);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
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).getAppBackupDetails(backupConfig, app.id, dataFilename, configFilename, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('createNewAppBackup: backup details %j', result);
|
||||
|
||||
async.series([
|
||||
addons.backupAddons.bind(null, app, manifest.addons),
|
||||
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD ].concat(result.backupScriptArguments))
|
||||
], function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debugApp(app, 'createNewAppBackup: %s done', dataFilename);
|
||||
|
||||
backupdb.add({ id: dataFilename, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, dataFilename);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
mailer.backupFailed(JSON.stringify(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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLocalDownloadPath(backupId, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(backupConfig.provider).getLocalFilePath(backupConfig, backupId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('getLocalDownloadPath: id:%s path:%s', backupId, result.filePath);
|
||||
|
||||
callback(null, result.filePath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeBackup(backupId, appBackupIds, callback) {
|
||||
assert.strictEqual(typeof backupId, 'string');
|
||||
assert(util.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('removeBackup: %s', backupId);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
api(backupConfig.provider).removeBackup(backupConfig, backupId, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
backupdb.del(backupId, function (error) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
|
||||
debug('removeBackup: %s done', backupId);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,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) {
|
||||
@@ -60,8 +63,8 @@ function Acme(options) {
|
||||
}
|
||||
|
||||
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()]);
|
||||
@@ -115,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);
|
||||
@@ -256,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
|
||||
@@ -266,7 +269,7 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
|
||||
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s"', result.body.status);
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
@@ -350,7 +353,7 @@ Acme.prototype.downloadChain = function (linkHeader, 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 !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -376,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)));
|
||||
|
||||