Compare commits
726 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d51a7178f | |||
| 9d52397bcc | |||
| 5098fbe061 | |||
| 7062aa4ac7 | |||
| d6fec4f2b9 | |||
| 86ef462c76 | |||
| c76e7a3f63 | |||
| 2516a08659 | |||
| 562fe30333 | |||
| 4e0eed4bb2 | |||
| b604caec72 | |||
| 6b409e9089 | |||
| 015d434358 | |||
| c8e448cb84 | |||
| 03924be491 | |||
| 2729cecf4a | |||
| 32e2377828 | |||
| fdb8139b03 | |||
| 4b25c8a5ad | |||
| ae930a7fe8 | |||
| 3b9144ba4d | |||
| be6ea3d4c1 | |||
| a2983e58b5 | |||
| a99e86a5df | |||
| 906ad80069 | |||
| ac65f765e5 | |||
| c5bfe82315 | |||
| 7035b3c18a | |||
| 2108c61d97 | |||
| 2bdbb47286 | |||
| 333b8970b8 | |||
| 5673cfe2be | |||
| 4429239dbc | |||
| b6ab9aa9f5 | |||
| 84bde6327f | |||
| d6f49eb54f | |||
| 3c8c5e158b | |||
| b3045b796f | |||
| c0febacc30 | |||
| f8ada91dc5 | |||
| d0e2ce9a9e | |||
| e157608992 | |||
| 8dbe0ddaf3 | |||
| b0cb18539c | |||
| 97b6d76694 | |||
| 9de6c8ee2b | |||
| cd28b1106b | |||
| b3a5dafee0 | |||
| eb4ab8defd | |||
| 639744e9cb | |||
| 6a942ab27a | |||
| 278f1d6d24 | |||
| 563eeca1a9 | |||
| 7a9c954646 | |||
| d768c36afb | |||
| 36ae3b267d | |||
| cd60f394d3 | |||
| 9aba90a6f7 | |||
| 68a8155f49 | |||
| 16695fd4ec | |||
| 9b6c6dc709 | |||
| 7923ed4f0d | |||
| 0b3d1c855c | |||
| d8273719d2 | |||
| c6d2c39ff7 | |||
| 6960afdf0b | |||
| 3a5000ab1d | |||
| 98951bab9e | |||
| 96fc3b8612 | |||
| 2b345b6c2d | |||
| 504662b466 | |||
| f56e6edbe4 | |||
| 191b84d389 | |||
| 8a4350d22e | |||
| cc6dae0f9e | |||
| 58528450e2 | |||
| ebf3559e60 | |||
| 57d20b2b32 | |||
| fd27240b26 | |||
| cad69d335c | |||
| 1f08cca355 | |||
| 7f4f525551 | |||
| b0037b6141 | |||
| 7956c8f58d | |||
| 330c9054b4 | |||
| d444d8552e | |||
| 595bf583c7 | |||
| 3386b99a29 | |||
| 5fd667cdaf | |||
| 4217db9e18 | |||
| b4717e2edb | |||
| 1d5465f21e | |||
| 2f1998fa67 | |||
| a7e998c030 | |||
| 8cc15726ec | |||
| 62e59868b4 | |||
| a64027f4af | |||
| f5a02930ec | |||
| 530ca20ee2 | |||
| f3b84ece3d | |||
| ca2d5957e4 | |||
| 7837214276 | |||
| 994202ca94 | |||
| ff7ceb1442 | |||
| 56545b7f41 | |||
| 586e78dfea | |||
| 92ede4c242 | |||
| 5ca2c2d564 | |||
| 9692aa3c08 | |||
| 10ad1028ae | |||
| 7155856b08 | |||
| 69aa771d44 | |||
| d164b5ae3a | |||
| b34d09f547 | |||
| 9e2850ffad | |||
| 480cface63 | |||
| 85aba589b8 | |||
| e890140aa9 | |||
| 53d56ef3a0 | |||
| b91674799b | |||
| 4bb864e2ac | |||
| 7db091525e | |||
| 695923ed75 | |||
| 1b43ccca6f | |||
| 96a0bad149 | |||
| 243ade15e1 | |||
| 9d3cf990d1 | |||
| 02bcff2223 | |||
| 8f388c86a6 | |||
| 8dc929f0ff | |||
| 509bd7e79b | |||
| 19c665d747 | |||
| cb09086ae8 | |||
| fa915d0b23 | |||
| a383f01406 | |||
| 1a46e80403 | |||
| e8cd230c12 | |||
| 0711dc2c5a | |||
| 486e72457d | |||
| 450e017bdb | |||
| c6d9cfc0d7 | |||
| a0b073d881 | |||
| 4dde16f987 | |||
| f7d2e262f4 | |||
| 34fedb5835 | |||
| ff491be976 | |||
| 7635482191 | |||
| b23001e43f | |||
| 06c8e8f0cb | |||
| ce2cd00fbf | |||
| 651af185c8 | |||
| 6951383ae0 | |||
| 37596e89b4 | |||
| 711fe37dad | |||
| 7fee3d0da0 | |||
| 45a61e9541 | |||
| bd0be2affc | |||
| 7812c0e5c2 | |||
| 7efb6d60bc | |||
| cd31e12bec | |||
| 87755c6097 | |||
| 73f56efe2c | |||
| 20eaa60a97 | |||
| b80f0082e9 | |||
| 1ff800a842 | |||
| 5b0abb1b17 | |||
| 178aa4794a | |||
| 76583cb2fa | |||
| aa484dc5b4 | |||
| 19a098d34b | |||
| db452d9bc0 | |||
| 90efb96635 | |||
| 0cee6de476 | |||
| 854d29330c | |||
| 34a3dd6d46 | |||
| 4787ee3301 | |||
| 7b547e7ae9 | |||
| fe5e31e528 | |||
| 841a838910 | |||
| 4f27fe4f1e | |||
| 96eab86341 | |||
| 95d7a991dc | |||
| dc309afbbd | |||
| 16d65d3665 | |||
| ccb340cf80 | |||
| 56b0f57e11 | |||
| 7c1e056152 | |||
| 08ffa99c78 | |||
| cdede5a009 | |||
| 4cadffa6ea | |||
| 04e13eac55 | |||
| 2b3ae69f63 | |||
| 8f4813f691 | |||
| 5b05baeced | |||
| 3d60e36c98 | |||
| 40c7bd114a | |||
| e0033b31f2 | |||
| 2d3bdda1c8 | |||
| fd40940ef5 | |||
| 6d58f65a1a | |||
| 44775e1791 | |||
| 4be1f4dd73 | |||
| 93bab552c9 | |||
| 023c03ddcd | |||
| a5bffad556 | |||
| 836348cbc0 | |||
| 1ac7570cfb | |||
| 0dceba8a1c | |||
| 599b070779 | |||
| c581e0ad09 | |||
| e14b59af5d | |||
| eff9de3ded | |||
| 4f128c6503 | |||
| 8dc9d4c083 | |||
| 21e3300396 | |||
| d136895598 | |||
| dac3eef57c | |||
| 2fac7dd736 | |||
| 74e2415308 | |||
| 41fae04b69 | |||
| 32a88a342c | |||
| b5bcde5093 | |||
| 68c36e8a18 | |||
| f6a9e1f4d8 | |||
| 2abd42096e | |||
| 922e214c52 | |||
| 6ce8899231 | |||
| cbfad632c2 | |||
| 7804aed5d7 | |||
| b90b1dbbbe | |||
| 020ec54264 | |||
| 0568093a2a | |||
| c9281bf863 | |||
| de451b2fe8 | |||
| ddf5c51737 | |||
| a33ccb32d2 | |||
| 0b03018a7b | |||
| 1b688410e7 | |||
| 6d031af012 | |||
| 67a5151070 | |||
| a4b299bf6e | |||
| 383d1eb406 | |||
| 3901144eae | |||
| 317c6db1d5 | |||
| 1e14f8e2b9 | |||
| 88fc7ca915 | |||
| b983e205d2 | |||
| 9cdbc6ba36 | |||
| 895f5f7398 | |||
| f41b08d573 | |||
| 3e21b6cad3 | |||
| 1a32482f66 | |||
| ee1e083f32 | |||
| ebd3a15140 | |||
| d93edc6375 | |||
| 3ed17f3a2a | |||
| 8d9cfbd3de | |||
| f142d34f83 | |||
| 357ca55dec | |||
| d7a8731027 | |||
| 9117c7d141 | |||
| 472020f90c | |||
| 2256a0dd3a | |||
| 458b5d1e32 | |||
| 1e6abed4aa | |||
| cdd4b426d5 | |||
| 75b60a2949 | |||
| 9ab34ee43a | |||
| 3c9d7706de | |||
| 8b5b954cbb | |||
| b2204925d3 | |||
| 63734155f2 | |||
| eb0ae3400a | |||
| db8db430b9 | |||
| c0b2b1c26d | |||
| 7da20e95e3 | |||
| f30f90e6be | |||
| 7f05b48bd7 | |||
| ea257b95d9 | |||
| e7c399c36a | |||
| d84666fb43 | |||
| 1eb33099af | |||
| e35dbd522f | |||
| db6474ef2a | |||
| e437671baf | |||
| f60d640c8e | |||
| 56c992e51b | |||
| 12ee7b9521 | |||
| c8de557ff7 | |||
| 90adaf29d7 | |||
| a71323f8b3 | |||
| 155995c7f3 | |||
| 319632e996 | |||
| 33d55318d8 | |||
| ec1abf8926 | |||
| 9a41f111b0 | |||
| 7ef6bd0d3f | |||
| 02f0bb3ea5 | |||
| e12b236617 | |||
| 6662a4d7d6 | |||
| 85315d8fc5 | |||
| 9f5a7e4c08 | |||
| ea0e61e6a4 | |||
| c301e9b088 | |||
| 70e861b106 | |||
| f5c6862627 | |||
| d845f1ae5b | |||
| 7c7d67c6c2 | |||
| c9fcbcc61c | |||
| 9ac06e7f85 | |||
| 6eafac2cad | |||
| 60cb0bdfb1 | |||
| 979956315c | |||
| 62ba031702 | |||
| 284cb7bee5 | |||
| 735c22bc98 | |||
| a2beed01a1 | |||
| 93fc6b06a2 | |||
| a327ce8a82 | |||
| f8374929ac | |||
| 5f93290fc7 | |||
| 4d139232bf | |||
| 804947f039 | |||
| 89fb2b57ff | |||
| 1262d11cb3 | |||
| 1ba72db4f8 | |||
| 7d2304e4a1 | |||
| ebf1dc1b08 | |||
| ce31f56eb6 | |||
| 7dd52779dc | |||
| 2eb5cab74b | |||
| db50382b18 | |||
| 32b061c768 | |||
| 740e85d28c | |||
| 568a7f814d | |||
| b99438e550 | |||
| bcdf90a8d9 | |||
| 536c16929b | |||
| d392293b50 | |||
| 16371d4528 | |||
| cdd0b48023 | |||
| 15cac726c4 | |||
| 6dc69a4d5d | |||
| c52dfcf52f | |||
| eaac13b1c1 | |||
| 3e83f3d4ee | |||
| 3845a8f02b | |||
| c932be77f8 | |||
| d89324162f | |||
| a0ef86f287 | |||
| 7255a86b32 | |||
| 81862bf934 | |||
| 81b7e5645c | |||
| 801367b68d | |||
| f2e8f325d1 | |||
| 138743b55f | |||
| 7f8db644d1 | |||
| c7e410c41b | |||
| 08f3b0b612 | |||
| a2782ef7a6 | |||
| 34fac8eb05 | |||
| 56338beae1 | |||
| 17e9f3b41d | |||
| 2c06b9325f | |||
| 2dfb91dcc9 | |||
| 9f20dfb237 | |||
| da2aecc76a | |||
| 7c72cd4399 | |||
| 5647b0430a | |||
| 7c94543da8 | |||
| 2118952120 | |||
| d45927cdf4 | |||
| c8e99e351e | |||
| fb56237122 | |||
| 89152fabde | |||
| 726463d497 | |||
| 055e41ac90 | |||
| 878878e5e4 | |||
| 7742c8a58e | |||
| 04476999f7 | |||
| 5bff7ebaa1 | |||
| 44742ea3ae | |||
| d6ea7fc3a0 | |||
| 2b49cde2c2 | |||
| 1008981306 | |||
| 146f3ad00e | |||
| 5219eff190 | |||
| abfd7b8aea | |||
| d98f64094e | |||
| a8d254738e | |||
| 1c9f2495e3 | |||
| aa4d95f352 | |||
| 558093eab1 | |||
| 865b041474 | |||
| 1888319313 | |||
| 0be7679619 | |||
| bbef6c2bc2 | |||
| be59267747 | |||
| b4477d26b7 | |||
| ce0afb3d80 | |||
| 0b5cd304ea | |||
| e54ad97fa7 | |||
| 66960ea785 | |||
| 72dd3026ca | |||
| 4c719de86c | |||
| c7a0b017b4 | |||
| 91c931b53c | |||
| 6f2b2adca9 | |||
| 3176bc1afa | |||
| b929adf2dd | |||
| f3d3b31bed | |||
| f17eaaf025 | |||
| 80d65acd0d | |||
| ba02d333d1 | |||
| 9b9d30c092 | |||
| d47de31744 | |||
| edc7efae5f | |||
| 18007be9e1 | |||
| d68ae4866c | |||
| f4b635a169 | |||
| d674d72508 | |||
| 6ee76f8ee4 | |||
| 06338e0a1f | |||
| 349c261238 | |||
| eb057fb399 | |||
| 5d739f012c | |||
| 741d56635f | |||
| 35404a2832 | |||
| 99505fc287 | |||
| a20b331095 | |||
| 06a9a82da0 | |||
| 03383eecbc | |||
| 89ae1a8b92 | |||
| 7061195059 | |||
| 9556d4b72c | |||
| dd764f1508 | |||
| 0a154339e6 | |||
| 2502b94f20 | |||
| 9b1b833fac | |||
| 848ca9817d | |||
| 9a159b50c6 | |||
| 11fb0d9850 | |||
| 3f925e5b96 | |||
| 714ae18658 | |||
| 226164c591 | |||
| 1d44d0a987 | |||
| babfb5efbb | |||
| badbb89c92 | |||
| 50e705fb25 | |||
| b9e0530ced | |||
| 9c793f1317 | |||
| cef93012bf | |||
| bd099cc844 | |||
| c1029ba3b0 | |||
| 152025baa7 | |||
| 94f0f48cba | |||
| 9b5c312aa1 | |||
| fdb488a4c3 | |||
| 69536e2263 | |||
| 3f8ea6f2ee | |||
| 3b035405b0 | |||
| 7b1a6e605b | |||
| 26ed331f8e | |||
| 29581b1f48 | |||
| 16ea13b88c | |||
| 2311107465 | |||
| 35cf9c454a | |||
| 4c2a57daf3 | |||
| ed9889af11 | |||
| 89dc2ec3f6 | |||
| 7811359b2f | |||
| 21c66915a6 | |||
| e3e99408d5 | |||
| 01f16659ac | |||
| 9e8f120fdd | |||
| 3b9b9a1629 | |||
| 9e2f43c3b1 | |||
| 588bb2df2f | |||
| 3c55ba1ea9 | |||
| 2a86216a4a | |||
| e3ea2323c5 | |||
| 6b55f3ae11 | |||
| f3496a421b | |||
| a4bba37606 | |||
| 56c4908365 | |||
| 18f6c4f2cd | |||
| d0ea1a4cf4 | |||
| aa75824cc6 | |||
| 61d5005c4b | |||
| 72d58f48e4 | |||
| 3f3b97dc16 | |||
| 8a05fdcb10 | |||
| 6fd3466db1 | |||
| f354baf685 | |||
| d009acf8e0 | |||
| fd479d04a0 | |||
| a3dc641be1 | |||
| a59f179e9d | |||
| 4128bc437b | |||
| e1b176594a | |||
| 35b11d7b22 | |||
| bd65e1f35d | |||
| a243478fff | |||
| f0fdc00e78 | |||
| a21210ab29 | |||
| 684e7df939 | |||
| 9be5f5d837 | |||
| 6c5fb67b58 | |||
| 616ec408d6 | |||
| 5969b4825c | |||
| 64c888fbdb | |||
| 8a0fe413ba | |||
| 270a1f4b95 | |||
| 8f4ed47b63 | |||
| 09997398b1 | |||
| 0b68d1c9aa | |||
| cc9904c8c7 | |||
| 16ab523cb2 | |||
| 20a75b7819 | |||
| 49e299b62d | |||
| 98a2090c72 | |||
| 38c542b05a | |||
| fc5fa621f3 | |||
| 6ec1a75cbb | |||
| bbba16cc9a | |||
| 564d3d563c | |||
| a858a4b4c1 | |||
| 2d6d8a7ea8 | |||
| 5b5ed9e043 | |||
| 801c40420c | |||
| c185b3db71 | |||
| 0f70b73e81 | |||
| d9865f9b0f | |||
| 59deb8b708 | |||
| 617fa98dee | |||
| c9cb1cabc4 | |||
| 92ab6b5aa4 | |||
| a66f250350 | |||
| 39200f4418 | |||
| 4f1c7742ef | |||
| e812cbcbe9 | |||
| 2e0670a5c1 | |||
| 92c92db595 | |||
| 1764567e1f | |||
| 7eeb8bcac1 | |||
| c718b4ccdd | |||
| 4f5ffc92a6 | |||
| 4c485f7bd0 | |||
| 7076a31821 | |||
| 68965f6da3 | |||
| b6a545d1f5 | |||
| c0afff4d13 | |||
| 604faa6669 | |||
| d94d1af7f5 | |||
| 9feb5dedd5 | |||
| 99948c4ed5 | |||
| 967bab678d | |||
| 135c296ac7 | |||
| e83ee48ed5 | |||
| 1539fe0906 | |||
| c06bddd19e | |||
| ceb78f21bb | |||
| 5af201d4ee | |||
| 794efb5ef5 | |||
| 31a9437b2c | |||
| 2b27e554fd | |||
| 4784b7b00e | |||
| e547a719f6 | |||
| 24f2d201ed | |||
| 792dfc731c | |||
| 6697b39e79 | |||
| db1eeff2c3 | |||
| fc624701bf | |||
| 591cc52944 | |||
| 67d840a1b3 | |||
| 8ffa951407 | |||
| af39c2c7ae | |||
| 5903c7d0bc | |||
| dbb79fc9e6 | |||
| ef1408fddb | |||
| 47ecb0e1cf | |||
| 55fad3d57e | |||
| 496a44d412 | |||
| 05721f73cc | |||
| 424c36ea49 | |||
| a38097e2f5 | |||
| b26cb4d339 | |||
| 3523974163 | |||
| a2bdd294a8 | |||
| f85bfdf451 | |||
| cfad186a6b | |||
| c8a9412995 | |||
| 318ea04efc | |||
| 90c1fd4c31 | |||
| fad6221750 | |||
| 9f0047478d | |||
| 591ef3271b | |||
| 9afbbde062 | |||
| 73e6e519a3 | |||
| 4268ba54bf | |||
| 47037b0066 | |||
| 05a6a36a62 | |||
| d72b1d8bd5 | |||
| 0f1a4422f5 | |||
| 7d06f9e1e3 | |||
| 1e4e76b0dd | |||
| 49d70f487e | |||
| 456cb22ac0 | |||
| ba1dfee5ca | |||
| 143a600a5c | |||
| 68b4bf0a7f | |||
| bc75d07391 | |||
| 7eaa3ef52e | |||
| af69ddc220 | |||
| b25d61fbb5 | |||
| 81a60b029d | |||
| 751fd8cc4b | |||
| 503e3d6ff2 | |||
| decbfe0505 | |||
| 379042616f | |||
| df2878bc2e | |||
| 1ff35461a2 | |||
| 7de94fff1b | |||
| 3236f70d8b | |||
| cf7cef19f9 | |||
| e159cdad5b | |||
| 2ddb533ef2 | |||
| 36a6e02269 | |||
| 6fbbf0ad61 | |||
| 1040fbddc6 | |||
| bbd63b2c57 | |||
| 905bdb1d27 | |||
| 11ce5ffa4c | |||
| b1854f82f2 | |||
| 745b7a26b7 | |||
| 764a38f23e | |||
| 7873fdc7bb | |||
| 76435460f0 | |||
| 7e3a54ff1b | |||
| 61789e3fda | |||
| 441c5fe534 | |||
| f30001d98b | |||
| fae0ba5678 | |||
| 7e592f34bd | |||
| 691f6c7c5c | |||
| f5eb5d545f | |||
| 91e4f6fcec | |||
| b759b12e90 | |||
| 103019984b | |||
| 01126aaeea | |||
| a6ab8ff02f | |||
| b89886a945 | |||
| d12b71f69c | |||
| 53c2ed3c82 | |||
| 148c8e6250 | |||
| 4a99eb105a | |||
| c5ca64af50 | |||
| 984b920fde | |||
| 54dae6827e | |||
| 58cf214bf2 | |||
| eeefdf5927 | |||
| 29c172deab | |||
| af1e83f12a | |||
| 3a3edc4617 | |||
| e13f52e371 | |||
| 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 |
@@ -681,3 +681,98 @@
|
||||
* 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.92.1]
|
||||
* Remove DO specific grub cmd line
|
||||
* Fix License text
|
||||
|
||||
[0.93.0]
|
||||
* Smoother upgrades
|
||||
|
||||
[0.94.0]
|
||||
* Cloudron domain can now be set after installation
|
||||
* Backups are now organized by directory
|
||||
* Document upgrading from Filesystem backend
|
||||
* Send certificate renewal errors, OOM errors to cloudron admins
|
||||
* Email bounce alerts are sent to the Cloudron owner
|
||||
|
||||
[0.94.1]
|
||||
* Suppress upgrade emails
|
||||
* Enable unattended upgrades
|
||||
* Standardize on using devicemapper for docker storage backend
|
||||
* Show detailed backup progress
|
||||
* Fix DNSBL issue in mail container
|
||||
* Fix issue where bounce emails were not sent to aliases
|
||||
* Remove tutorial
|
||||
* Restart mail container on certificate change
|
||||
|
||||
[0.97.0]
|
||||
* Fix missing app icon issue
|
||||
* Fix issue where box sends out crash reports incessantly
|
||||
* (API) Allow memory limit to be set to -1 (unlimited)
|
||||
* (API) Move developmentMode flag from manifest to apps route
|
||||
|
||||
[0.98.0]
|
||||
* Send stat on whether email is enabled
|
||||
* Fix bug where heartbeat was sent for self-hosted Cloudrons
|
||||
* Make Cloudron function even when disk is full
|
||||
* Fix thunderbird connection issue
|
||||
* Send more detailed logs for backup failures
|
||||
* Restart nginx if it crashed automatically
|
||||
* Support all DNS providers for managed Cloudrons
|
||||
* Add granular configuration for auto-updates
|
||||
|
||||
[0.99.0]
|
||||
* Fix bug where ports <= 1023 were not reserved
|
||||
* Cleanup graphs UI
|
||||
* Polish webadmin UI
|
||||
* Fix bug where hard disk size was detected incorrectly
|
||||
|
||||
[0.99.1]
|
||||
* Fix bug with duplicate nginx configs
|
||||
|
||||
[0.100.0]
|
||||
* Improve DNS notifications for email
|
||||
* Do not enable HSTS for subdomains
|
||||
|
||||
[0.100.1]
|
||||
* Fix crash when fetching mail records
|
||||
* Fix crash in LDAP server when username and displayName are empty
|
||||
|
||||
[0.101.0]
|
||||
* New base image 0.10.0
|
||||
* Better error handling of unpurchase errors
|
||||
* Validate that cloudron domain name is a subdomain of public suffic list
|
||||
* Add canada and london to S3 backup regions
|
||||
* Bundle Font Awesome as part of webadmin
|
||||
* Fix crash in custom certiicate validation
|
||||
* Get A+ rating in SSL Check
|
||||
* More robust detection and injection of SPF record
|
||||
* Add azure, lightsail, linode, ovh, vultr to provider list
|
||||
|
||||
[0.102.0]
|
||||
* Fix issue where SPF record check was only done 5 times (updated 'async')
|
||||
* Make auto-generated self-signed cert load quickly on Firefox
|
||||
* Ensure we download docker images and have an app data volume on app re-configure
|
||||
* Improve certificate renewal erorr message
|
||||
* Fix disk usage graph
|
||||
* Show Repair UI for errored apps
|
||||
|
||||
[0.102.1]
|
||||
* Add terms link when signing up for Cloudron.io account
|
||||
* Fix issue where Cloudrons with many apps (> 35) were unable to backup
|
||||
* Improve wording of DNS Setup
|
||||
|
||||
[0.103.0]
|
||||
* Do not send crash logs and other notifications to support@cloudron.io for self-hosted instances
|
||||
* Make auto-generated self-signed cert load quickly on Firefox (take 2)
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ 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
|
||||
Copyright (C) 2016 Cloudron UG
|
||||
|
||||
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
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 14 KiB |
Executable
+193
@@ -0,0 +1,193 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
INSTANCE_TYPE="t2.micro"
|
||||
BLOCK_DEVICE="DeviceName=/dev/sda1,Ebs={VolumeSize=20,DeleteOnTermination=true,VolumeType=gp2}"
|
||||
SSH_KEY_NAME="id_rsa_yellowtent"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
ami_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="prod"
|
||||
image_id=""
|
||||
|
||||
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:,region:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) ami_name="$2"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--region)
|
||||
case "$2" in
|
||||
"us-east-1")
|
||||
image_id="ami-6edd3078"
|
||||
security_group="sg-a5e17fd9"
|
||||
subnet_id="subnet-b8fbc0f1"
|
||||
;;
|
||||
"eu-central-1")
|
||||
image_id="ami-5aee2235"
|
||||
security_group="sg-19f5a770" # everything open on eu-central-1
|
||||
subnet_id=""
|
||||
;;
|
||||
*)
|
||||
echo "Unknown aws region $2"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
export AWS_DEFAULT_REGION="$2" # used by the aws cli tool
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# TODO fix this
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_ACCESS_SECRET}"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
|
||||
readonly SSH="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${image_id}" ]]; then
|
||||
echo "--region is required (us-east-1 or eu-central-1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
function wait_for_ssh() {
|
||||
echo "=> Waiting for ssh connection"
|
||||
while true; do
|
||||
echo -n "."
|
||||
|
||||
if $SSH ubuntu@${server_ip} echo "hello"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${ami_name}" ]]; then
|
||||
ami_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
fi
|
||||
|
||||
echo "=> Create EC2 instance"
|
||||
id=$(aws ec2 run-instances --image-id "${image_id}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${security_group}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}" --subnet-id "${subnet_id}" --associate-public-ip-address \
|
||||
| $JSON Instances \
|
||||
| $JSON 0.InstanceId)
|
||||
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Instance created ID $id"
|
||||
|
||||
echo "=> Waiting for instance to get a public IP"
|
||||
while true; do
|
||||
server_ip=$(aws ec2 describe-instances --instance-ids ${id} \
|
||||
| $JSON Reservations.0.Instances \
|
||||
| $JSON 0.PublicIpAddress)
|
||||
|
||||
if [[ ! -z "${server_ip}" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Got public IP ${server_ip}"
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Fetching cloudron-setup"
|
||||
while true; do
|
||||
|
||||
if $SSH ubuntu@${server_ip} wget "https://cloudron.io/cloudron-setup" -O "cloudron-setup"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "=> Running cloudron-setup"
|
||||
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ec2" --skip-reboot
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Removing ssh key"
|
||||
$SSH ubuntu@${server_ip} sudo rm /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys
|
||||
|
||||
echo "=> Creating AMI"
|
||||
image_id=$(aws ec2 create-image --instance-id "${id}" --name "${ami_name}" | $JSON ImageId)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Creating AMI with Id ${image_id}"
|
||||
|
||||
echo "=> Waiting for AMI to be created"
|
||||
while true; do
|
||||
state=$(aws ec2 describe-images --image-ids ${image_id} \
|
||||
| $JSON Images \
|
||||
| $JSON 0.State)
|
||||
|
||||
if [[ "${state}" == "available" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "=> Deleting EC2 instance"
|
||||
|
||||
while true; do
|
||||
state=$(aws ec2 terminate-instances --instance-id "${id}" \
|
||||
| $JSON TerminatingInstances \
|
||||
| $JSON 0.CurrentState.Name)
|
||||
|
||||
if [[ "${state}" == "shutting-down" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
echo ""
|
||||
echo "New AMI is: ${image_id}"
|
||||
echo ""
|
||||
@@ -10,7 +10,7 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
installer_revision=$(git rev-parse HEAD)
|
||||
revision=$(git rev-parse HEAD)
|
||||
box_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
@@ -28,7 +28,7 @@ eval set -- "${args}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) installer_revision="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) box_name="$2"; destroy_server="no"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--) break;;
|
||||
@@ -73,7 +73,7 @@ function get_pretty_revision() {
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${installer_revision}")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${box_name}" ]]; then
|
||||
# if you change this, change the regexp is appstore/janitor.js
|
||||
@@ -138,13 +138,13 @@ 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} caas"; then
|
||||
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh caas"; then
|
||||
echo "Init script failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down server with id : ${server_id}"
|
||||
$ssh202 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
$ssh22 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
|
||||
# wait 10 secs for actual shutdown
|
||||
echo "Waiting for 10 seconds for server to shutdown"
|
||||
|
||||
@@ -31,7 +31,7 @@ function create_droplet() {
|
||||
|
||||
local image_region="sfo1"
|
||||
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||
local box_size="512mb"
|
||||
local box_size="1gb"
|
||||
|
||||
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||
|
||||
|
||||
@@ -2,293 +2,94 @@
|
||||
|
||||
set -euv -o pipefail
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly USER_HOME="/home/${USER}"
|
||||
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
|
||||
readonly INSTALLER_REVISION="${1:-master}"
|
||||
readonly PROVIDER="${2:-generic}"
|
||||
readonly USER_DATA_FILE="/root/user_data.img"
|
||||
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
readonly arg_provider="${1:-generic}"
|
||||
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
|
||||
|
||||
function die {
|
||||
echo $1
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
|
||||
|
||||
echo "==== Create User ${USER} ===="
|
||||
if ! id "${USER}"; then
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
echo "=== Upgrade ==="
|
||||
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
|
||||
echo "=== Setting up firewall ==="
|
||||
# clear tables and set default policy
|
||||
iptables -F # flush all chains
|
||||
iptables -X # delete all chains
|
||||
# default policy for filter table
|
||||
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
|
||||
echo "==> Installing required packages"
|
||||
|
||||
# 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
|
||||
# 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 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.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
|
||||
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||
|
||||
# loopback
|
||||
iptables -A INPUT -i lo -j ACCEPT
|
||||
iptables -A OUTPUT -o lo -j ACCEPT
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
btrfs-tools \
|
||||
build-essential \
|
||||
cron \
|
||||
curl \
|
||||
dmsetup \
|
||||
iptables \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
rcconf \
|
||||
swaks \
|
||||
unattended-upgrades \
|
||||
unbound
|
||||
|
||||
# prevent DoS
|
||||
# iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -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 (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
|
||||
|
||||
echo "==== Install btrfs tools ==="
|
||||
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
|
||||
groupadd docker
|
||||
cat > /etc/systemd/system/docker.socket <<EOF
|
||||
[Unit]
|
||||
Description=Docker Socket for the API
|
||||
PartOf=docker.service
|
||||
|
||||
[Socket]
|
||||
ListenStream=/var/run/docker.sock
|
||||
SocketMode=0660
|
||||
SocketUser=root
|
||||
SocketGroup=docker
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
EOF
|
||||
cat > /etc/systemd/system/docker.service <<EOF
|
||||
[Unit]
|
||||
Description=Docker Application Container Engine
|
||||
After=network.target docker.socket
|
||||
Requires=docker.socket
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --dns 127.0.0.1
|
||||
MountFlags=slave
|
||||
LimitNOFILE=1048576
|
||||
LimitNPROC=1048576
|
||||
LimitCORE=infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
echo "=== Setup btrfs data ==="
|
||||
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}
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
|
||||
# give docker sometime to start up and create iptables rules
|
||||
# those rules come in after docker has started, and we want to wait for them to be sure iptables-save has all of them
|
||||
sleep 10
|
||||
|
||||
# Disable forwarding to metadata route from containers
|
||||
iptables -I FORWARD -d 169.254.169.254 -j DROP
|
||||
|
||||
# ubuntu will restore iptables from this file automatically. this is here so that docker's chain is saved to this file
|
||||
mkdir /etc/iptables && iptables-save > /etc/iptables/rules.v4
|
||||
|
||||
echo "=== Enable memory accounting =="
|
||||
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
|
||||
|
||||
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
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-6.9.2
|
||||
curl -sL https://nodejs.org/dist/v6.9.2/node-v6.9.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.9.2
|
||||
ln -sf /usr/local/node-6.9.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-6.9.2/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(' '));")
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
echo "==> Installing Docker"
|
||||
apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
|
||||
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get -y update
|
||||
|
||||
echo "Pulling images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}"
|
||||
done
|
||||
else
|
||||
echo "No infra_versions.js found, skipping image download"
|
||||
# create systemd drop-in file
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
apt-get -y --allow-downgrades install docker-engine=1.12.5-0~ubuntu-xenial # apt-cache madison docker-engine
|
||||
apt-mark hold docker-engine # do not update docker
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
if [[ "${storage_driver}" != "devicemapper" ]]; then
|
||||
echo "Docker is using "${storage_driver}" instead of devicemapper"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==== Install nginx ===="
|
||||
apt-get -y install nginx-full
|
||||
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y install grub2
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
echo "==== Install build-essential ===="
|
||||
apt-get -y install build-essential rcconf
|
||||
echo "==> Downloading docker images"
|
||||
if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
|
||||
echo "No infra_versions.js found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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-5.7
|
||||
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
|
||||
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo "==== Install pwgen and swaks awscli ===="
|
||||
apt-get -y install pwgen swaks awscli
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}"
|
||||
done
|
||||
|
||||
echo "==== Install collectd ==="
|
||||
echo "==> Install collectd"
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
update-rc.d -f collectd remove
|
||||
|
||||
# this simply makes it explicit that we run logrotate via cron. it's already part of base ubuntu
|
||||
echo "==== Install logrotate ==="
|
||||
apt-get install -y cron logrotate
|
||||
systemctl enable cron
|
||||
|
||||
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 cloudron-version tool ===="
|
||||
npm install -g cloudron-version@0.1.1
|
||||
|
||||
# Restore iptables before docker
|
||||
echo "==== Install iptables-restore systemd script ===="
|
||||
cat > /etc/systemd/system/iptables-restore.service <<EOF
|
||||
[Unit]
|
||||
Description=IPTables Restore
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Allocate swap files
|
||||
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
|
||||
# On ubuntu ec2 we use cloud-init https://wiki.archlinux.org/index.php/Cloud-init
|
||||
echo "==== Install 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 sshd.service nginx.service
|
||||
After=cloud-init.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/cloudron-system-setup.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable iptables-restore
|
||||
systemctl enable cloudron-system-setup
|
||||
|
||||
# Configure systemd
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
-i /etc/systemd/journald.conf
|
||||
|
||||
# When rotating logs, systemd kills journald too soon sometimes
|
||||
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
|
||||
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
|
||||
-i /lib/systemd/system/systemd-journald.service
|
||||
|
||||
sync
|
||||
|
||||
# Configure time
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Give user access to system logs
|
||||
apt-get -y install acl
|
||||
usermod -a -G systemd-journal ${USER}
|
||||
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
|
||||
chown root:systemd-journal /var/log/journal
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
# 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/^#\?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
|
||||
systemctl reload sshd
|
||||
fi
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||
|
||||
+5
-21
@@ -151,6 +151,8 @@ If `altDomain` is set, the app can be accessed from `https://<altDomain>`.
|
||||
* `SAMEORIGIN` - allows embedding from the same domain as the app. This is the default.
|
||||
* `ALLOW-FROM https://example.com/` - allows this app to be embedded from example.com
|
||||
|
||||
`memoryLimit` is the maximum memory this app can use (in bytes) including swap. If set to 0, the app uses the `memoryLimit` value set in the manifest. If set to -1, the app gets unlimited memory.
|
||||
|
||||
Read more about the options at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options).
|
||||
|
||||
Response (200):
|
||||
@@ -447,7 +449,7 @@ POST `/api/v1/apps/:appId/configure` <scope>admin</scope>
|
||||
|
||||
Re-configures an existing app with id `appId`.
|
||||
|
||||
Configuring an app won't preserve existing data. Cloudron apps are written in a way to support reconfiguring
|
||||
Configuring an app preserves existing data. Cloudron apps are written in a way to support reconfiguring
|
||||
any of the parameters listed below without loss of data.
|
||||
|
||||
Request:
|
||||
@@ -964,24 +966,6 @@ Response (204):
|
||||
{}
|
||||
```
|
||||
|
||||
### Tutorial
|
||||
|
||||
POST `/api/v1/profile/tutorial` <scope>profile</scope>
|
||||
|
||||
Toggles display of the tutorial when the token owner logs in.
|
||||
|
||||
Request:
|
||||
```
|
||||
{
|
||||
showTutorial: <boolean>
|
||||
}
|
||||
```
|
||||
|
||||
Response (204):
|
||||
```
|
||||
{}
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
### Get auto update pattern
|
||||
@@ -1099,7 +1083,7 @@ This is currently internal API and is documented here for completeness.
|
||||
Response(200):
|
||||
```
|
||||
{
|
||||
"provider": <string> // 'caas' or 'route53' or 'digitalocean'
|
||||
"provider": <string> // 'caas' or 'route53' or 'digitalocean' or 'noop' or 'manual'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1132,7 +1116,7 @@ POST `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
|
||||
|
||||
Sets the email configuration. The Cloudron has a built-in email server for users.
|
||||
This configuration can be used to enable or disable the email server. Note that
|
||||
the Cloudron will always be able to send email on behalf of apps, regardless of
|
||||
the Cloudron will always be able to send email on behalf of apps, regardless of
|
||||
this setting.
|
||||
|
||||
Request:
|
||||
|
||||
@@ -44,13 +44,6 @@ Applications can be broadly categorized based on their user management as follow
|
||||
|
||||
* 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`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Overview
|
||||
|
||||
The application's Dockerfile must specify the FROM base image to be `cloudron/base:0.9.0`.
|
||||
The application's Dockerfile must specify the FROM base image to be `cloudron/base:0.10.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.
|
||||
@@ -17,16 +17,16 @@ install it yourself.
|
||||
|
||||
* Apache 2.4.18
|
||||
* Composer 1.2.0
|
||||
* Go 1.5.4, 1.6.3
|
||||
* Go 1.6.4, 1.7.5 (install under `/usr/local/go-<version>`)
|
||||
* Gunicorn 19.4.5
|
||||
* Java 1.8
|
||||
* Maven 3.3.9
|
||||
* Mongo 2.6.10
|
||||
* MySQL Client 5.7.13
|
||||
* MySQL Client 5.7.17
|
||||
* 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)
|
||||
* Node 0.10.48, 0.12.18, 4.7.3, 6.9.5 (installed under `/usr/local/node-<version>`) [more information](#node-js)
|
||||
* Perl 5.22.1
|
||||
* PHP 7.0.8
|
||||
* PHP 7.0.13
|
||||
* Postgresql client 9.5.4
|
||||
* Python 2.7.12
|
||||
* Redis 3.0.6
|
||||
@@ -41,16 +41,16 @@ The base image can be inspected by installing [Docker](https://docs.docker.com/i
|
||||
|
||||
Once installed, pull down the base image locally using the following command:
|
||||
```
|
||||
docker pull cloudron/base:0.9.0
|
||||
docker pull cloudron/base:0.10.0
|
||||
```
|
||||
|
||||
To inspect the base image:
|
||||
```
|
||||
docker run -ti cloudron/base:0.9.0 /bin/bash
|
||||
docker run -ti cloudron/base:0.10.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`.
|
||||
image with an incorrect image id. The image id of `cloudron/base:0.10.0` is `5ec8ca8525be`.
|
||||
|
||||
# The `cloudron` user
|
||||
|
||||
|
||||
@@ -47,12 +47,14 @@ Type: object
|
||||
Required: no
|
||||
|
||||
Allowed keys
|
||||
* [email](addons.html#email)
|
||||
* [ldap](addons.html#ldap)
|
||||
* [localstorage](addons.html#localstorage)
|
||||
* [mongodb](addons.html#mongodb)
|
||||
* [mysql](addons.html#mysql)
|
||||
* [oauth](addons.html#oauth)
|
||||
* [postgresql](addons.html#postgresql)
|
||||
* [recvmail](addons.html#recvmail)
|
||||
* [redis](addons.html#redis)
|
||||
* [sendmail](addons.html#sendmail)
|
||||
|
||||
@@ -76,7 +78,7 @@ The `author` field contains the name and email of the app developer (or company)
|
||||
|
||||
Example:
|
||||
```
|
||||
"author": "Cloudron Inc <girish@cloudron.io>"
|
||||
"author": "Cloudron UG <girish@cloudron.io>"
|
||||
```
|
||||
|
||||
## changelog
|
||||
@@ -93,26 +95,6 @@ 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
|
||||
@@ -150,20 +132,6 @@ 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
|
||||
@@ -186,9 +154,10 @@ 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.
|
||||
The `httpPort` field contains the TCP port on which your app is listening for HTTP requests. This
|
||||
is the HTTP port the Cloudron will use to access your app internally.
|
||||
|
||||
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.
|
||||
|
||||
+122
-109
@@ -1,8 +1,7 @@
|
||||
# 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.
|
||||
Linode, OVH, Scaleway, Vultr etc. Cloudron also runs well on a home server or company intranet.
|
||||
|
||||
If you run into any trouble following this guide, ask us at our [chat](https://chat.cloudron.io).
|
||||
|
||||
@@ -20,67 +19,39 @@ The Cloudron requires a domain name when it is installed. Apps are installed int
|
||||
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.
|
||||
apps on existing subdomains (so, it is safe to reuse an existing domain that runs other services).
|
||||
|
||||
# CLI Tool
|
||||
# Cloud Server
|
||||
|
||||
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>
|
||||
DigitalOcean and EC2 (Amazon Web Services) are frequently tested by us.
|
||||
|
||||
## 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
|
||||
|
||||
Both DigitalOcean and EC2 from 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/)
|
||||
* [hosttech](https://www.hosttech.ch/?promocode=53619290)
|
||||
* [Linode](https://www.linode.com/?r=f68d816692c49141e91dd4cef3305da457ac0f75)
|
||||
* [OVH](https://www.ovh.com/)
|
||||
* [Rosehosting](https://secure.rosehosting.com/clientarea/?affid=661)
|
||||
* [Scaleway](https://www.scaleway.com/)
|
||||
* [So you Start](https://www.soyoustart.com/)
|
||||
* [Vultr](https://www.vultr.com/)
|
||||
* [Vultr](http://www.vultr.com/?ref=7110116-3B)
|
||||
|
||||
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.
|
||||
|
||||
Cloudron has a built-in firewall and ports are opened and closed dynamically, as and when
|
||||
apps are installed, re-configured or removed. For this reason, be sure to open all TCP and
|
||||
UDP traffic to the server and leave the traffic management to the Cloudron.
|
||||
|
||||
### Linode
|
||||
|
||||
Since Linode does not manage SSH keys, be sure to add the public key to
|
||||
@@ -91,74 +62,51 @@ Since Linode does not manage SSH keys, be sure to add the public key to
|
||||
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/master/scripts/cloudron-setup
|
||||
wget https://cloudron.io/cloudron-setup
|
||||
chmod +x cloudron-setup
|
||||
./cloudron-setup --domain <domain> --provider <digitalocean|ec2|generic|scaleway> --encryption-key <key>
|
||||
./cloudron-setup --provider <azure|digitalocean|ec2|lightsail|linode|ovh|rosehosting|scaleway|vultr|generic>
|
||||
```
|
||||
|
||||
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.
|
||||
**cloudron-setup** takes the following arguments:
|
||||
|
||||
* `--provider` is the name of your VPS provider. If the name is not on the list, simply
|
||||
choose `generic`. The `generic` provider mostly will work fine.
|
||||
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.
|
||||
|
||||
* `--encryption-key` is the key to be used for encrypting backup data.
|
||||
|
||||
Optional arguments for installation:
|
||||
|
||||
* `--tls-provider` is the name of the SSL/TLS certificate backend. Defaults to Let's encrypt.
|
||||
If Let's encrypt is not wanted, specifying `fallback` will always use the fallback wildcard certificate.
|
||||
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. This is useful when restoring a Cloudron from a backup.
|
||||
the latest version. You can set this to an older version when restoring a Cloudron from a backup.
|
||||
|
||||
* `--restore-url` is an URL to the backup to restore to.
|
||||
* `--restore-url` is a backup URL to restore from.
|
||||
|
||||
## Domain setup
|
||||
|
||||
Once the setup script completes, the server will reboot, then visit your server by its
|
||||
IP address (`https://ip`) to complete the installation.
|
||||
|
||||
## Finish setup
|
||||
The setup website will show a certificate warning. Accept the self-signed certificate
|
||||
and proceed to the domain setup.
|
||||
|
||||
Once the setup script completes, 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>`.
|
||||
|
||||
**If apps do not start after installation, a server restart may be required to let bootloader changes come into action.**
|
||||
|
||||
## DNS
|
||||
|
||||
Cloudron has to be given the API credentials for configuring your domain under `Certs & Domains`
|
||||
in the web UI.
|
||||
Currently, only subdomains of the [Public Suffix List](https://publicsuffix.org/) are supported.
|
||||
For example, `example.com`, `example.co.uk` will work fine. Choosing other non-registrable
|
||||
domain names like `cloudron.example.com` will not work.
|
||||
|
||||
### Route 53
|
||||
|
||||
@@ -205,17 +153,21 @@ If your domain *does not* use Route 53 or Digital Ocean, setup a wildcard (`*`)
|
||||
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
|
||||
## Finish Setup
|
||||
|
||||
Once the domain setup is done, the Cloudron will configure the DNS and get a SSL certificate. It will automatically redirect to `https://my.<domain>`.
|
||||
|
||||
# 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_`.
|
||||
backups have the prefix `app_`. The platform state is backed up independently with the
|
||||
prefix `box_`.
|
||||
|
||||
By default, backups reside in `/var/backups`. Having backups reside in the same location as the
|
||||
server instance is dangerous and it must be changed to an external storage location like `S3`
|
||||
as soon as possible.
|
||||
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
|
||||
## Amazon S3
|
||||
|
||||
Provide S3 backup credentials in the `Settings` page and leave the endpoint field empty.
|
||||
|
||||
@@ -247,7 +199,10 @@ for most use-cases.
|
||||
}
|
||||
```
|
||||
|
||||
### Minio S3
|
||||
The `Encryption key` is an arbitrary passphrase used to encrypt the backups. Keep the passphrase safe; it is
|
||||
required to decrypt the backups when restoring the Cloudron.
|
||||
|
||||
## 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.
|
||||
@@ -272,6 +227,8 @@ The information to be copied to the Cloudron's backup settings form may look sim
|
||||
|
||||
<img src="/docs/img/minio_backup_config.png" class="shadow"><br/>
|
||||
|
||||
The `Encryption key` is an arbitrary passphrase used to encrypt the backups. Keep the passphrase safe; it is
|
||||
required to decrypt the backups when restoring the Cloudron.
|
||||
|
||||
# Email
|
||||
|
||||
@@ -288,12 +245,25 @@ 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.
|
||||
* If you are unable to receive mail, first thing to check is if your VPS provider lets you
|
||||
receive mail on port 25.
|
||||
|
||||
* 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 - New accounts frequently have port 25 blocked. Write to their support to
|
||||
unblock your server.
|
||||
|
||||
* EC2, Lightsail & Scaleway - Edit your security group to allow email.
|
||||
|
||||
* Setup a Reverse DNS PTR record to be setup for the `my` subdomain.
|
||||
**Note:** PTR records are a feature of your VPS provider and not your domain provider.
|
||||
|
||||
* You can verify the PTR record [https://mxtoolbox.com/ReverseLookup.aspx](here).
|
||||
|
||||
* AWS EC2 & Lightsail - 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>`.
|
||||
your droplet to `my.<domain>`. Note that some new Digital Ocean accounts have [port 25 blocked](https://www.digitalocean.com/community/questions/port-25-smtp-external-access).
|
||||
|
||||
* Linode - Follow this [guide](https://www.linode.com/docs/networking/dns/setting-reverse-dns).
|
||||
|
||||
* Scaleway - Edit your security group to allow email. You can also set a PTR record on the interface with your
|
||||
`my.<domain>`.
|
||||
@@ -301,9 +271,36 @@ reputation should be easy to get back.
|
||||
* 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.
|
||||
|
||||
* When using wildcard or manual DNS backends, you have to setup the DMARC, MX records manually.
|
||||
|
||||
* 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.
|
||||
|
||||
# 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 installed & run on a Laptop or PC</b>
|
||||
|
||||
Once installed, you can install, configure, list, backup and restore apps from the command line.
|
||||
|
||||
## 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.
|
||||
|
||||
# Updates
|
||||
|
||||
Apps installed from the Cloudron Store are automatically updated every night.
|
||||
@@ -320,24 +317,26 @@ 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).
|
||||
An **upgrade** requires a new OS image. This process involves creating a new server from scratch
|
||||
with the latest code and restoring it from the last backup.
|
||||
|
||||
To upgrade follow these steps closely:
|
||||
|
||||
* Create a new backup - `cloudron machine backup create <domain>`
|
||||
* Create a new backup - `cloudron machine backup create`
|
||||
|
||||
* List the latest backup - `cloudron machine backup list <domain>`
|
||||
* List the latest backup - `cloudron machine backup list`
|
||||
|
||||
* Make the latest box backup (files starting with `backup_`) public. This can be done from the AWS S3 console as seen here:
|
||||
* Make the backup available for the new cloudron instance:
|
||||
|
||||
<img src="/docs/img/aws_backup_public.png" class="shadow haze"><br/>
|
||||
* `S3` - When storing backup ins S3, make the latest box backup public - files starting with `box_` (from v0.94.0) or `backup_`. This can be done from the AWS S3 console as seen here:
|
||||
|
||||
* Copy the new public URL of the latest backup for use as the `--restore-url` below.
|
||||
<img src="/docs/img/aws_backup_public.png" class="shadow haze"><br/>
|
||||
|
||||
<img src="/docs/img/aws_backup_link.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/>
|
||||
|
||||
* `File system` - When storing backups in `/var/backups`, you have to make the box and the app backups available to the new Cloudron instance's `/var/backups`. This can be achieved in a variety of ways depending on the situation: like scp'ing the backup files to the machine before installation, mounting the external backup hard drive into the new Cloudron's `/var/backup` OR downloading a copy of the backup using `cloudron machine backup download` and uploading them to the new machine. After doing so, pass `file:///var/backups/<path to box backup>` 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 `--encryption-key` and `--restore-url` flags.
|
||||
@@ -346,24 +345,30 @@ To upgrade follow these steps closely:
|
||||
Similar to the initial installation, a Cloudron upgrade looks like:
|
||||
```
|
||||
$ ssh root@newserverip
|
||||
> wget https://git.cloudron.io/cloudron/box/raw/master/scripts/cloudron-setup
|
||||
> wget https://cloudron.io/cloudron-setup
|
||||
> chmod +x cloudron-setup
|
||||
> ./cloudron-setup --domain <domain> --provider <digitalocean|ec2|generic|scaleway> --encryption-key <key> --restore-url <publicS3Url>
|
||||
> ./cloudron-setup --provider <digitalocean|ec2|generic|scaleway> --domain <example.com> --encryption-key <key> --restore-url <publicS3Url>
|
||||
```
|
||||
|
||||
Note: When upgrading an old version of Cloudron (<= 0.94.0), pass the `--version 0.94.1` flag and then continue updating
|
||||
from that.
|
||||
|
||||
* 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>`
|
||||
* Select the backup - `cloudron machine backup list`
|
||||
|
||||
* 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.
|
||||
* Make the backup public
|
||||
|
||||
* `S3` - Make the box backup publicly readable - files starting with `box_` (from v0.94.0) or `backup_`. This can be done from the AWS S3 console. Once the box has restored, you can make it private again.
|
||||
|
||||
* `File system` - When storing backups in `/var/backups`, you have to make the box and the app backups available to the new Cloudron instance's `/var/backups`. This can be achieved in a variety of ways depending on the situation: like scp'ing the backup files to the new machine before Cloudron installation OR mounting an external backup hard drive into the new Cloudron's `/var/backup` OR downloading a copy of the backup using `cloudron machine backup download` and uploading them to the new machine. After doing so, pass `file:///var/backups/<path to box backup>` 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.
|
||||
When running the setup script, pass in the `version`, `encryption-key`, `domain` 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).
|
||||
|
||||
@@ -377,6 +382,14 @@ You can SSH into your Cloudron and collect logs:
|
||||
* `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>`.
|
||||
|
||||
# Alerts
|
||||
|
||||
The Cloudron will notify the Cloudron administrator via email if apps go down, run out of memory, have updates
|
||||
available etc.
|
||||
|
||||
You will have to setup a 3rd party service like [Cloud Watch](https://aws.amazon.com/cloudwatch/) or [UptimeRobot](http://uptimerobot.com/) to monitor the Cloudron itself. You can use `https://my.<domain>/api/v1/cloudron/status`
|
||||
as the health check URL.
|
||||
|
||||
# Help
|
||||
|
||||
If you run into any problems, join us at our [chat](https://chat.cloudron.io) or [email us](mailto:support@cloudron.io).
|
||||
|
||||
@@ -75,7 +75,7 @@ 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
|
||||
FROM cloudron/base:0.10.0
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
|
||||
@@ -171,7 +171,7 @@ Login successful.
|
||||
Build scheduled with id 76cebfdd-7822-4f3d-af17-b3eb393ae604
|
||||
Downloading source
|
||||
Building
|
||||
Step 0 : FROM cloudron/base:0.9.0
|
||||
Step 0 : FROM cloudron/base:0.10.0
|
||||
---> 97583855cc0c
|
||||
Step 1 : ADD server.js /app/code
|
||||
---> b09b97ecdfbc
|
||||
@@ -333,7 +333,7 @@ and modify our Dockerfile to look like this:
|
||||
File `tutorial/Dockerfile`
|
||||
|
||||
```dockerfile
|
||||
FROM cloudron/base:0.9.0
|
||||
FROM cloudron/base:0.10.0
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
ADD package.json /app/code/package.json
|
||||
|
||||
+21
-16
@@ -5,8 +5,8 @@ This tutorial outlines how to package an existing web application for the Cloudr
|
||||
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
|
||||
* Create a Dockerfile for your application. If your application already has a Dockerfile, it
|
||||
is a good starting point for packaging for the Cloudron. 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
|
||||
@@ -79,27 +79,27 @@ console.log("Server running at port 8000");
|
||||
The Dockerfile contains instructions on how to create an image for your application.
|
||||
|
||||
```Dockerfile
|
||||
FROM cloudron/base:0.9.0
|
||||
FROM cloudron/base:0.10.0
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
|
||||
CMD [ "/usr/local/node-4.2.1/bin/node", "/app/code/server.js" ]
|
||||
CMD [ "/usr/local/node-4.4.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. This approach conserves space on the Cloudron since
|
||||
Docker images tend to be quiet large.
|
||||
Docker images tend to be quite large and also helps us to do a security audit on apps more easily.
|
||||
|
||||
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.
|
||||
node.js. We use Node 4.4.7 here.
|
||||
|
||||
This Dockerfile can be built and run locally as:
|
||||
```
|
||||
docker build -t tutorial .
|
||||
docker run -p 8000:8000 -ti tutorial
|
||||
docker run -p 8000:8000 -t tutorial
|
||||
```
|
||||
|
||||
## Manifest
|
||||
@@ -188,7 +188,7 @@ 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
|
||||
Step 1 : FROM cloudron/base:0.10.0
|
||||
---> be9fc6312b2d
|
||||
Step 2 : ADD server.js /app/code/server.js
|
||||
---> 10513e428d7a
|
||||
@@ -271,14 +271,18 @@ You can also execute arbitrary commands:
|
||||
$ cloudron exec env # display the env variables that your app is running with
|
||||
```
|
||||
|
||||
### DevelopmentMode
|
||||
### Debugging
|
||||
|
||||
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`.
|
||||
An app can be placed in `debug` mode by passing `--debug` to `cloudron install` or `cloudron configure`.
|
||||
Doing so, runs the app in a non-readonly rootfs and unlimited memory. By default, this will also ignore
|
||||
the `RUN` command specified in the Dockerfile. The developer can then interactively test the app and
|
||||
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.
|
||||
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.
|
||||
|
||||
You can turn off debugging mode using `cloudron configure --no-debug`.
|
||||
|
||||
# Addons
|
||||
|
||||
@@ -385,6 +389,8 @@ 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.
|
||||
|
||||
An app can determine it's memory limit by reading `/sys/fs/cgroup/memory/memory.limit_in_bytes`.
|
||||
|
||||
## Authentication
|
||||
|
||||
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
|
||||
@@ -429,9 +435,8 @@ 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.
|
||||
You should now be able to visit `/#/appstore/<appid>?version=<appversion>` on your
|
||||
Cloudron to 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>`.
|
||||
|
||||
+27
-1
@@ -40,7 +40,16 @@ gulp.task('3rdparty', function () {
|
||||
// JavaScript
|
||||
// --------------
|
||||
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-update'], function () {});
|
||||
if (argv.help || argv.h) {
|
||||
console.log('Supported arguments for "gulp develop":');
|
||||
console.log(' --client-id <clientId>');
|
||||
console.log(' --client-secret <clientSecret>');
|
||||
console.log(' --api-origin <cloudron api uri>');
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function () {});
|
||||
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
@@ -94,6 +103,22 @@ gulp.task('js-setup', function () {
|
||||
.pipe(gulp.dest('webadmin/dist/js'));
|
||||
});
|
||||
|
||||
gulp.task('js-setupdns', function () {
|
||||
// needs special treatment for error handling
|
||||
var uglifyer = uglify();
|
||||
uglifyer.on('error', function (error) {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
|
||||
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(concat('setupdns.js', { newLine: ';' }))
|
||||
.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();
|
||||
@@ -162,6 +187,7 @@ gulp.task('watch', ['default'], function () {
|
||||
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
|
||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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,100 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
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=$([[ -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
|
||||
chmod +x /home/yellowtent/provision.sh
|
||||
|
||||
arg_source_tarball_url=""
|
||||
arg_data=""
|
||||
arg_data_file=""
|
||||
|
||||
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
|
||||
|
||||
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}"
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# 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}"
|
||||
chown -R yellowtent.yellowtent "${BOX_SRC_DIR}"
|
||||
|
||||
# create a start file for testing. %q escapes args
|
||||
(echo -e "#!/bin/bash\n"; printf "%q " "${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}") > /home/yellowtent/setup_start.sh
|
||||
chmod +x /home/yellowtent/setup_start.sh
|
||||
|
||||
echo "Calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
var dbm = require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var url = require('url');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs'),
|
||||
async = require('async'),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN resetToken VARCHAR(128) DEFAULT ""', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DELETE FROM tokens', [], function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE authcodes ADD COLUMN expiresAt BIGINT NOT NULL', function (error) {
|
||||
@@ -13,4 +12,4 @@ exports.down = function(db, callback) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE appPortBindings ADD COLUMN environmentVariable VARCHAR(128) NOT NULL', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE appPortBindings DROP COLUMN containerPort', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DELETE FROM tokens', [], function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN version', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN healthy, ADD COLUMN health VARCHAR(128)', [], function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupId VARCHAR(128)', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// everyday at 1am
|
||||
@@ -8,5 +7,4 @@ exports.up = function(db, callback) {
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE * FROM settings WHERE name="autoupdate_pattern"', [ ], callback);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
var safe = require('safetydance');
|
||||
var type = dbm.dataType;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
|
||||
@@ -12,4 +12,3 @@ exports.up = function(db, callback) {
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE * FROM settings WHERE name="time_zone"', [ ], callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN lastManifestJson VARCHAR(2048)', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE lastManifestJson lastBackupConfigJson VARCHAR(2048)', [], function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN oldConfigJson VARCHAR(2048)', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('DELETE FROM settings', [ ], callback);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE accessRestriction accessRestrictionJson VARCHAR(2048)', [], function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE groups(" +
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var async = require('async');
|
||||
|
||||
var ADMIN_GROUP_ID = 'admin'; // see groups.js
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE backups(" +
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
|
||||
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups CHANGE filename id VARCHAR(128)', [], function (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) UNIQUE', [], function (error) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
'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);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE eventlog(" +
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
'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,' +
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// imports mailbox entries for existing users
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps MODIFY installationProgress TEXT', [], function (error) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
dbm = dbm || require('db-migrate');
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN xFrameOptions VARCHAR(512)', function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT id FROM users', function (error, results) {
|
||||
@@ -14,4 +13,3 @@ exports.up = function(db, callback) {
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DELETE * FROM settings WHERE name="mail_config"', [ ], callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var dbm = dbm || require('db-migrate');
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
@@ -71,4 +71,3 @@ exports.down = function(db, callback) {
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN sso BOOLEAN DEFAULT 1', function (error) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
var dbm = global.dbm || require('db-migrate');
|
||||
var type = dbm.dataType;
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN debugModeJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN debugModeJson ', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups MODIFY dependsOn TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups MODIFY dependsOn VARCHAR(4096)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -19,7 +19,6 @@ 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(
|
||||
@@ -61,13 +60,14 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL UNIQUE,
|
||||
dnsRecordId VARCHAR(512),
|
||||
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates (unused)
|
||||
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
|
||||
debugModeJson TEXT, // options for development mode
|
||||
|
||||
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
|
||||
|
||||
@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS backups(
|
||||
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 */
|
||||
dependsOn TEXT, /* comma separate list of objects this backup depends on */
|
||||
state VARCHAR(16) NOT NULL,
|
||||
|
||||
PRIMARY KEY (filename));
|
||||
|
||||
Generated
+1639
-2657
File diff suppressed because it is too large
Load Diff
+10
-13
@@ -13,11 +13,11 @@
|
||||
"node >=4.0.0 <=4.1.1"
|
||||
],
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"async": "^2.1.4",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"body-parser": "^1.13.1",
|
||||
"checksum": "^0.1.1",
|
||||
"cloudron-manifestformat": "^2.5.1",
|
||||
"cloudron-manifestformat": "^2.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^0.1.0",
|
||||
"connect-timeout": "^1.5.0",
|
||||
@@ -25,15 +25,17 @@
|
||||
"cookie-session": "^1.1.0",
|
||||
"cron": "^1.0.9",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.9.2",
|
||||
"db-migrate": "^0.10.0-beta.20",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^2.2.0",
|
||||
"dockerode": "^2.2.10",
|
||||
"ejs": "^2.2.4",
|
||||
"ejs-cli": "^1.2.0",
|
||||
"express": "^4.12.4",
|
||||
"express-rate-limit": "^2.6.0",
|
||||
"express-session": "^1.11.3",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
"ini": "^1.3.4",
|
||||
"json": "^9.0.3",
|
||||
"ldapjs": "^1.0.0",
|
||||
"mime": "^1.3.4",
|
||||
@@ -58,20 +60,16 @@
|
||||
"proxy-middleware": "^0.13.0",
|
||||
"safetydance": "^0.1.1",
|
||||
"semver": "^4.3.6",
|
||||
"showdown": "^1.4.4",
|
||||
"showdown": "^1.6.0",
|
||||
"split": "^1.0.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.3",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^4.9.0",
|
||||
"x509": "^0.2.4"
|
||||
"validator": "^4.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "*",
|
||||
"bootstrap-sass": "^3.3.3",
|
||||
"deep-extend": "^0.4.1",
|
||||
"del": "^1.1.1",
|
||||
@@ -81,7 +79,7 @@
|
||||
"gulp-concat": "^2.4.3",
|
||||
"gulp-cssnano": "^2.1.0",
|
||||
"gulp-ejs": "^1.0.0",
|
||||
"gulp-sass": "^2.0.1",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-serve": "^1.0.0",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
@@ -89,10 +87,9 @@
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^1.0.0",
|
||||
"mocha": "*",
|
||||
"nock": "^3.4.0",
|
||||
"nock": "^9.0.2",
|
||||
"node-sass": "^3.0.0-alpha.0",
|
||||
"request": "^2.65.0",
|
||||
"sinon": "^1.12.2",
|
||||
"yargs": "^3.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
+190
-156
@@ -7,59 +7,53 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ask() {
|
||||
# http://djm.me/ask
|
||||
local prompt default REPLY
|
||||
|
||||
while true; do
|
||||
|
||||
if [ "${2:-}" = "Y" ]; then
|
||||
prompt="Y/n"
|
||||
default=Y
|
||||
elif [ "${2:-}" = "N" ]; then
|
||||
prompt="y/N"
|
||||
default=N
|
||||
else
|
||||
prompt="y/n"
|
||||
default=
|
||||
fi
|
||||
|
||||
# Ask the question (not using "read -p" as it uses stderr not stdout)
|
||||
echo -n "$1 [$prompt] "
|
||||
|
||||
# Read the answer (use /dev/tty in case stdin is redirected from somewhere else)
|
||||
read REPLY </dev/tty
|
||||
|
||||
# Default?
|
||||
if [ -z "$REPLY" ]; then
|
||||
REPLY=$default
|
||||
fi
|
||||
|
||||
# Check if the reply is valid
|
||||
case "$REPLY" in
|
||||
Y*|y*) return 0 ;;
|
||||
N*|n*) return 1 ;;
|
||||
esac
|
||||
|
||||
done
|
||||
}
|
||||
if [[ $(lsb_release -rs) != "16.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly INSTALLER_REVISION=master
|
||||
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"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
readonly MINIMUM_DISK_SIZE_GB="19" # this is the size of "/" and required to fit in docker images 19 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_MEMORY="980" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989)
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
# copied from cloudron-resize-fs.sh
|
||||
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
|
||||
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
|
||||
|
||||
# verify the system has minimum requirements met
|
||||
if [[ "${physical_memory}" -lt "${MINIMUM_MEMORY}" ]]; then
|
||||
echo "Error: Cloudron requires atleast 1GB physical memory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
|
||||
echo "Error: Cloudron requires atleast 20GB disk space (Disk space on ${disk_device} is ${disk_size_gb}GB)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
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"
|
||||
requestedVersion="latest"
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
dataJson=""
|
||||
prerelease="false"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
|
||||
args=$(getopt -o "" -l "domain:,help,provider:,encryption-key:,restore-url:,tls-provider:,version:,versions-url:,api-server:" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,provider:,encryption-key:,restore-url:,tls-provider:,version:,versions-url:,api-server:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -70,156 +64,196 @@ while true; do
|
||||
--encryption-key) encryptionKey="$2"; shift 2;;
|
||||
--restore-url) restoreUrl="$2"; shift 2;;
|
||||
--tls-provider) tlsProvider="$2"; shift 2;;
|
||||
--version) version="$2"; shift 2;;
|
||||
--dns-provider) dnsProvider="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
apiServerOrigin="https://api.dev.cloudron.io"
|
||||
versionsUrl="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
|
||||
tlsProvider="le-staging"
|
||||
prerelease="true"
|
||||
elif [[ "$2" == "staging" ]]; then
|
||||
apiServerOrigin="https://api.staging.cloudron.io"
|
||||
versionsUrl="https://s3.amazonaws.com/staging-cloudron-releases/versions.json"
|
||||
tlsProvider="le-staging"
|
||||
prerelease="true"
|
||||
fi
|
||||
shift 2;;
|
||||
--versions-url) versionsUrl="$2"; shift 2;;
|
||||
--api-server) apiServer="$2"; shift 2;;
|
||||
--api-server) apiServerOrigin="$2"; shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--data) dataJson="$2"; shift 2;;
|
||||
--prerelease) prerelease="true"; shift;;
|
||||
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${domain}" ]]; then
|
||||
echo "--domain is required"
|
||||
exit 1
|
||||
fi
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "azure" && \
|
||||
"${provider}" != "digitalocean" && \
|
||||
"${provider}" != "ec2" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
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 [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
|
||||
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${encryptionKey}" ]]; then
|
||||
echo "--encryption-key for backup encryption is required"
|
||||
exit 1
|
||||
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
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "##############################################"
|
||||
echo " Cloudron Setup (${version}) "
|
||||
echo " Cloudron Setup (${requestedVersion}) "
|
||||
echo "##############################################"
|
||||
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
|
||||
|
||||
ip=$(dig a "my.${domain}" +short)
|
||||
echo ""
|
||||
if [[ -z "${ip}" ]]; then
|
||||
echo "No DNS A record found for my.${domain}"
|
||||
echo "First create a new DNS A record for my.${domain} pointing to the public ip address of this server."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found DNS A record for my.${domain} pointing to:"
|
||||
echo "${ip}"
|
||||
echo ""
|
||||
if ! ask "Is this correct?"; then
|
||||
echo "Please ensure that the A record for my.${domain} is pointing to the correct public ip address of this server."
|
||||
exit 1
|
||||
fi
|
||||
echo " Join us at https://chat.cloudron.io for any questions."
|
||||
echo ""
|
||||
|
||||
echo "=> Downloading initialization script"
|
||||
if ! curl -s "${INIT_BASESYSTEM_SCRIPT_URL}" > /tmp/initializeBaseUbuntuImage.sh; then
|
||||
echo "Could not download initialization script"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories"
|
||||
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
|
||||
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl)"
|
||||
exit 1
|
||||
fi
|
||||
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
|
||||
if [[ "${sourceTarballUrl}" == "" ]]; then
|
||||
releaseJson=$($curl -s "${versionsUrl}")
|
||||
if [[ "$requestedVersion" == "latest" ]]; then
|
||||
pre=$([[ "${prerelease}" == "true" ]] && echo "null" || echo "-pre")
|
||||
version=$(echo "${releaseJson}" | python3 -c "import json,sys,collections;obj=json.load(sys.stdin, object_pairs_hook=collections.OrderedDict);latest=list(v for v in obj if '${pre}' not in v)[-1];print(latest)")
|
||||
else
|
||||
version="${requestedVersion}"
|
||||
fi
|
||||
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj[sys.argv[1]]["sourceTarballUrl"])' "${version}"); then
|
||||
echo "No source code for version ${requestedVersion}"
|
||||
exit 1
|
||||
fi
|
||||
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}"
|
||||
},
|
||||
"backupConfig" : {
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
# Build data
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${restoreUrl}" ]]; then
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
},
|
||||
"dnsConfig": {
|
||||
"provider": "${dnsProvider}"
|
||||
},
|
||||
"backupConfig" : {
|
||||
"provider": "filesystem",
|
||||
"backupFolder": "/var/backups",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"updateConfig": {
|
||||
"prerelease": ${prerelease}
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
)
|
||||
else
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"restore": {
|
||||
"url": "${restoreUrl}",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
else
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"boxVersionsUrl": "${versionsUrl}",
|
||||
"fqdn": "${domain}",
|
||||
"provider": "${provider}",
|
||||
"apiServerOrigin": "${apiServer}",
|
||||
"restore": {
|
||||
"url": "${restoreUrl}",
|
||||
"key": "${encryptionKey}"
|
||||
},
|
||||
"tlsConfig": {
|
||||
"provider": "${tlsProvider}"
|
||||
},
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
data="${dataJson}"
|
||||
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 "=> Downloading version ${version} ..."
|
||||
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||
|
||||
if ! $curl -sL "${sourceTarballUrl}" | tar -zxf - -C "${box_src_tmp_dir}"; then
|
||||
echo "Could not download source tarball. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
|
||||
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${provider}" "../src" &>> "${LOG_FILE}"; then
|
||||
echo "Init script failed. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
rm "${DATA_FILE}"
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready"
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
if journalctl -u box -a | grep "platformReady: configured, resuming tasks" >/dev/null; then
|
||||
break
|
||||
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
|
||||
[[ -z "$domain" ]] && break # with no domain, we are up and running
|
||||
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Visit https://my.${domain} to finish setup"
|
||||
echo ""
|
||||
if [[ -n "${domain}" ]]; then
|
||||
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
|
||||
else
|
||||
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
|
||||
fi
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
|
||||
systemctl reboot
|
||||
fi
|
||||
|
||||
@@ -2,55 +2,37 @@
|
||||
|
||||
set -eu
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:,publish,no-upload" -n "$0" -- "$@")
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -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=""
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--revision) commitish="$2"; shift 2;;
|
||||
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
|
||||
--no-upload) upload="no"; shift;;
|
||||
--publish) publish="yes"; shift;;
|
||||
--output) bundle_file="$2"; shift 2;;
|
||||
--) 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
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v4.1.1" ]]; then
|
||||
echo "This script requires node 4.1.1"
|
||||
if [[ "$(node --version)" != "v6.9.2" ]]; then
|
||||
echo "This script requires node 6.9.2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -101,23 +83,5 @@ echo "Create final tarball"
|
||||
echo "Cleaning up ${bundle_dir}"
|
||||
rm -rf "${bundle_dir}"
|
||||
|
||||
if [[ "${upload}" == "yes" ]]; then
|
||||
echo "Uploading bundle to S3"
|
||||
# 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
|
||||
echo "Tarball preserved at ${bundle_file}"
|
||||
else
|
||||
rm "${bundle_file}"
|
||||
fi
|
||||
echo "Tarball saved at ${bundle_file}"
|
||||
|
||||
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
|
||||
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
|
||||
|
||||
arg_data=""
|
||||
|
||||
args=$(getopt -o "" -l "data:,data-file:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--data) arg_data="$2"; shift 2;;
|
||||
--data-file) arg_data=$(cat $2); shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
for try in `seq 1 10`; do
|
||||
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||
|
||||
# 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 ! id "${USER}" 2>/dev/null; then
|
||||
useradd "${USER}" -m
|
||||
fi
|
||||
|
||||
if [[ "${is_update}" == "yes" ]]; then
|
||||
echo "Setting up update splash screen"
|
||||
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # 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
|
||||
|
||||
echo "==> installer: switching the box code"
|
||||
rm -rf "${BOX_SRC_DIR}"
|
||||
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
|
||||
|
||||
echo "==> installer: calling box setup script"
|
||||
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||
@@ -1,57 +0,0 @@
|
||||
This document gives the design of this setup code.
|
||||
|
||||
box code should be delivered in the form of a (docker) container.
|
||||
This is not the case currently but we want to do structure the code
|
||||
in spirit that way.
|
||||
|
||||
### container.sh
|
||||
This contains code that essential goes into Dockerfile.
|
||||
|
||||
This file contains static configuration over a base image. Currently,
|
||||
the yellowtent user is created in the installer base image but it
|
||||
could very well be placed here.
|
||||
|
||||
The idea is that the installer would simply remove the old box container
|
||||
and replace it with a new one for an update.
|
||||
|
||||
Because we do not package things as Docker yet, we should be careful
|
||||
about the code here. We have to expect remains of an older setup code.
|
||||
For example, older systemd or nginx configs might be around.
|
||||
|
||||
The config directory is _part_ of the container and is not a VOLUME.
|
||||
Which is to say that the files will be nuked from one update to the next.
|
||||
|
||||
The data directory is a VOLUME. Contents of this directory are expected
|
||||
to survive an update. This is a good place to place config files that
|
||||
are "dynamic" and need to survive restarts. For example, the infra
|
||||
version (see below) or the mysql/postgresql data etc.
|
||||
|
||||
### start.sh
|
||||
* It is called in 3 modes - new, update, restore.
|
||||
|
||||
* The first thing this does is to do the static container.sh setup.
|
||||
|
||||
* It then downloads any box restore data and restores the box db from the
|
||||
backup.
|
||||
|
||||
* It then proceeds to call the db-migrate script.
|
||||
|
||||
* It then does dynamic configuration like setting up nginx, collectd.
|
||||
|
||||
* It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf.
|
||||
|
||||
* box services are then started
|
||||
|
||||
setup_infra.sh
|
||||
This setups containers like graphite, mail and the addons containers.
|
||||
|
||||
Containers are relaunched based on the INFRA_VERSION. The script compares
|
||||
the version here with the version in the file DATA_DIR/INFRA_VERSION.
|
||||
|
||||
If they match, the containers are not recreated and nothing is to be done.
|
||||
nginx, collectd configs are part of data already and containers are running.
|
||||
|
||||
If they do not match, it deletes all containers (including app containers) and starts
|
||||
them all afresh. Important thing here is that, DATA_DIR is never removed across
|
||||
updates. So, it is only the containers being recreated and not the data.
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${script_dir}/../node_modules/.bin/json"
|
||||
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
json="${source_dir}/../node_modules/.bin/json"
|
||||
|
||||
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
|
||||
arg_api_server_origin=""
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This file can be used in Dockerfile
|
||||
|
||||
readonly container_files="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/container"
|
||||
|
||||
readonly CONFIG_DIR="/home/yellowtent/configs"
|
||||
readonly DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
########## create config directory
|
||||
rm -rf "${CONFIG_DIR}"
|
||||
sudo -u yellowtent mkdir "${CONFIG_DIR}"
|
||||
|
||||
########## systemd
|
||||
rm -f /etc/systemd/system/janitor.*
|
||||
cp -r "${container_files}/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable cloudron.target
|
||||
|
||||
########## sudoers
|
||||
rm -f /etc/sudoers.d/yellowtent
|
||||
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
||||
|
||||
########## collectd
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
|
||||
########## apparmor docker profile
|
||||
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl restart apparmor
|
||||
|
||||
########## nginx
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
ln -s "${DATA_DIR}/nginx" /etc/nginx
|
||||
|
||||
########## mysql
|
||||
cp "${container_files}/mysql.cnf" /etc/mysql/mysql.cnf
|
||||
|
||||
########## Enable services
|
||||
update-rc.d -f collectd defaults
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ set -eu -o pipefail
|
||||
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly BOX_SRC_DIR="/home/yellowtent/box"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly DATA_DIR="/home/yellowtent/data"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
|
||||
@@ -28,11 +28,11 @@ existing_infra="none"
|
||||
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" \
|
||||
${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\", \"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" \
|
||||
${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\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
|
||||
+218
-123
@@ -2,149 +2,257 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
echo "==== Cloudron Start ===="
|
||||
echo "==> Cloudron Start"
|
||||
|
||||
readonly USER="yellowtent"
|
||||
readonly BOX_SRC_DIR="/home/${USER}/box"
|
||||
readonly DATA_DIR="/home/${USER}/data"
|
||||
readonly CONFIG_DIR="/home/${USER}/configs"
|
||||
readonly SETUP_PROGRESS_JSON="/home/yellowtent/setup/website/progress.json"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
readonly DATA_FILE="/root/user_data.img"
|
||||
readonly HOME_DIR="/home/${USER}"
|
||||
readonly BOX_SRC_DIR="${HOME_DIR}/box"
|
||||
readonly DATA_DIR="${HOME_DIR}/data" # app and platform data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||
|
||||
# keep this is sync with config.js appFqdn()
|
||||
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
|
||||
admin_origin="https://${admin_fqdn}"
|
||||
|
||||
readonly is_update=$([[ -f "${CONFIG_DIR}/cloudron.conf" ]] && echo "true" || echo "false")
|
||||
|
||||
set_progress() {
|
||||
local percent="$1"
|
||||
local message="$2"
|
||||
|
||||
echo "==== ${percent} - ${message} ===="
|
||||
echo "==> ${percent} - ${message}"
|
||||
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
|
||||
}
|
||||
|
||||
set_progress "1" "Create container"
|
||||
$script_dir/container.sh
|
||||
|
||||
set_progress "5" "Adjust system settings"
|
||||
set_progress "20" "Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
hostnamectl set-hostname "${arg_fqdn}"
|
||||
|
||||
set_progress "10" "Ensuring directories"
|
||||
echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# caas has ssh on port 202
|
||||
if [[ "${arg_provider}" == "caas" ]]; then
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
|
||||
else
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,22,443,587,993,4190 -j ACCEPT
|
||||
fi
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||
iptables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -t filter -A CLOUDRON -j DROP
|
||||
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null; then
|
||||
iptables -t filter -I INPUT -j CLOUDRON
|
||||
fi
|
||||
|
||||
# so it gets restored across reboot
|
||||
mkdir -p /etc/iptables && iptables-save > /etc/iptables/rules.v4
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
temp_file=$(mktemp)
|
||||
# create systemd drop-in. some apps do not work with aufs
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper --dns=172.18.0.1 --dns-search=." > "${temp_file}"
|
||||
|
||||
systemctl enable docker
|
||||
# restart docker if options changed
|
||||
if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /etc/systemd/system/docker.service.d/cloudron.conf "${temp_file}" >/dev/null; then
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
mv "${temp_file}" /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
fi
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
# caas has ssh on port 202 and we disable password login
|
||||
if [[ "${arg_provider}" == "caas" ]]; then
|
||||
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
|
||||
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
|
||||
systemctl reload sshd
|
||||
fi
|
||||
|
||||
echo "==> Setup btrfs data"
|
||||
if [[ ! -d "${DATA_DIR}" ]]; then
|
||||
echo "==> Mounting loopback btrfs"
|
||||
truncate -s "8192m" "${DATA_FILE}" # 8gb start (this will get resized dynamically by cloudron-resize-fs.service)
|
||||
mkfs.btrfs -L UserDataHome "${DATA_FILE}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
mount -t btrfs -o loop,nosuid "${DATA_FILE}" ${DATA_DIR}
|
||||
fi
|
||||
|
||||
# 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/dkim/${arg_fqdn}"
|
||||
mkdir -p "${DATA_DIR}/box/acme" # acme keys
|
||||
echo "==> Ensuring directories"
|
||||
if ! btrfs subvolume show "${DATA_DIR}/mail" &> /dev/null; then
|
||||
# Migrate mail data to new format
|
||||
docker stop mail || true # otherwise the move below might fail if mail container writes in the middle
|
||||
rm -rf "${DATA_DIR}/mail" # this used to be mail container's run directory
|
||||
btrfs subvolume create "${DATA_DIR}/mail"
|
||||
[[ -d "${DATA_DIR}/box/mail" ]] && mv "${DATA_DIR}/box/mail/"* "${DATA_DIR}/mail"
|
||||
rm -rf "${DATA_DIR}/box/mail"
|
||||
fi
|
||||
mkdir -p "${DATA_DIR}/graphite"
|
||||
mkdir -p "${DATA_DIR}/mail/dkim"
|
||||
|
||||
mkdir -p "${DATA_DIR}/mysql"
|
||||
mkdir -p "${DATA_DIR}/postgresql"
|
||||
mkdir -p "${DATA_DIR}/mongodb"
|
||||
mkdir -p "${DATA_DIR}/snapshots"
|
||||
mkdir -p "${DATA_DIR}/addons"
|
||||
mkdir -p "${DATA_DIR}/addons/mail"
|
||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${DATA_DIR}/acme" # acme challenges
|
||||
mkdir -p "${DATA_DIR}/acme"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
if btrfs subvolume show "${DATA_DIR}/box" &> /dev/null; then
|
||||
# Migrate box data out of data volume
|
||||
mv "${DATA_DIR}/box/"* "${BOX_DATA_DIR}"
|
||||
btrfs subvolume delete "${DATA_DIR}/box"
|
||||
fi
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
|
||||
echo "==> Configuring journald"
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
-i /etc/systemd/journald.conf
|
||||
|
||||
# When rotating logs, systemd kills journald too soon sometimes
|
||||
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
|
||||
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
|
||||
-i /lib/systemd/system/systemd-journald.service
|
||||
|
||||
# Give user access to system logs
|
||||
usermod -a -G systemd-journal ${USER}
|
||||
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
|
||||
chown root:systemd-journal /var/log/journal
|
||||
systemctl daemon-reload
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
echo "==> Creating config directory"
|
||||
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# 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!)
|
||||
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
|
||||
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
|
||||
echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable iptables-restore
|
||||
|
||||
# For logrotate
|
||||
systemctl enable --now cron
|
||||
|
||||
# ensure unbound runs
|
||||
systemctl restart unbound
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
|
||||
echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring nginx"
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
ln -s "${DATA_DIR}/nginx" /etc/nginx
|
||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||
mkdir -p "${DATA_DIR}/nginx/cert"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||
if ! grep "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
||||
# default nginx service file does not restart on crash
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
systemctl start nginx
|
||||
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${BOX_DATA_DIR}/version"
|
||||
|
||||
# remove old snapshots. if we do want to keep this around, we will have to fix the chown -R below
|
||||
# which currently fails because these are readonly fs
|
||||
echo "Cleaning up snapshots"
|
||||
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
|
||||
# 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
|
||||
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
|
||||
# wait for all running mysql jobs
|
||||
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
|
||||
while true; do
|
||||
if ! systemctl list-jobs | grep mysql; then break; fi
|
||||
echo "Waiting for mysql jobs..."
|
||||
sleep 1
|
||||
done
|
||||
systemctl restart mysql
|
||||
else
|
||||
systemctl start mysql
|
||||
fi
|
||||
|
||||
readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
if [[ -n "${arg_restore_url}" ]]; then
|
||||
set_progress "15" "Downloading restore data"
|
||||
set_progress "30" "Downloading restore data"
|
||||
|
||||
echo "Downloading backup: ${arg_restore_url} and key: ${arg_restore_key}"
|
||||
echo "==> Downloading backup: ${arg_restore_url} and key: ${arg_restore_key}"
|
||||
|
||||
while true; do
|
||||
if $curl -L "${arg_restore_url}" | openssl aes-256-cbc -d -pass "pass:${arg_restore_key}" | tar -zxf - -C "${DATA_DIR}/box"; then break; fi
|
||||
if $curl -L "${arg_restore_url}" | openssl aes-256-cbc -d -pass "pass:${arg_restore_key}" \
|
||||
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,data/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
|
||||
echo "Failed to download data, trying again"
|
||||
done
|
||||
|
||||
set_progress "21" "Setting up MySQL"
|
||||
if [[ -f "${DATA_DIR}/box/box.mysqldump" ]]; then
|
||||
echo "Importing existing database into MySQL"
|
||||
mysql -u root -p${mysql_root_password} box < "${DATA_DIR}/box/box.mysqldump"
|
||||
set_progress "35" "Setting up MySQL"
|
||||
if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then
|
||||
echo "==> Importing existing database into MySQL"
|
||||
mysql -u root -p${mysql_root_password} box < "${BOX_DATA_DIR}/box.mysqldump"
|
||||
fi
|
||||
fi
|
||||
|
||||
set_progress "25" "Migrating data"
|
||||
set_progress "40" "Migrating data"
|
||||
sudo -u "${USER}" -H bash <<EOF
|
||||
set -eu
|
||||
cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@localhost/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
|
||||
set_progress "28" "Setup collectd"
|
||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||
systemctl restart collectd
|
||||
|
||||
set_progress "30" "Setup nginx"
|
||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||
|
||||
# generate these for update code paths as well to overwrite splash
|
||||
admin_cert_file="${DATA_DIR}/nginx/cert/host.cert"
|
||||
admin_key_file="${DATA_DIR}/nginx/cert/host.key"
|
||||
if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs/${admin_fqdn}.key" ]]; then
|
||||
admin_cert_file="${DATA_DIR}/box/certs/${admin_fqdn}.cert"
|
||||
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}\", \"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
|
||||
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}/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 "65" "Creating cloudron.conf"
|
||||
sudo -u yellowtent -H bash <<EOF
|
||||
set -eu
|
||||
echo "Creating cloudron.conf"
|
||||
echo "==> Creating cloudron.conf"
|
||||
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"version": "${arg_version}",
|
||||
@@ -166,69 +274,56 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
||||
"appBundle": ${arg_app_bundle}
|
||||
}
|
||||
CONF_END
|
||||
# pass these out-of-band because they have new lines which interfere with json
|
||||
if [[ -n "${arg_tls_cert}" && -n "${arg_tls_key}" ]]; then
|
||||
echo "${arg_tls_cert}" > "${CONFIG_DIR}/host.cert"
|
||||
echo "${arg_tls_key}" > "${CONFIG_DIR}/host.key"
|
||||
fi
|
||||
|
||||
echo "Creating config.json for webadmin"
|
||||
echo "==> Creating config.json for webadmin"
|
||||
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
|
||||
{
|
||||
"webServerOrigin": "${arg_web_server_origin}"
|
||||
}
|
||||
CONF_END
|
||||
EOF
|
||||
|
||||
# Add Backup Configuration
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
echo "Add Backup Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||
fi
|
||||
|
||||
# Add DNS Configuration
|
||||
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||
echo "Add DNS Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||
fi
|
||||
|
||||
# Add Update Configuration
|
||||
if [[ ! -z "${arg_update_config}" ]]; then
|
||||
echo "Add Update Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
|
||||
fi
|
||||
|
||||
# Add TLS Configuration
|
||||
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||
echo "Add TLS Config"
|
||||
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||
fi
|
||||
|
||||
# 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 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\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
fi
|
||||
|
||||
echo "Add SDK api client"
|
||||
mysql -u root -p${mysql_root_password} \
|
||||
-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"
|
||||
set_progress "60" "Starting Cloudron"
|
||||
systemctl start cloudron.target
|
||||
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
set_progress "85" "Reloading nginx"
|
||||
nginx -s reload
|
||||
|
||||
set_progress "100" "Done"
|
||||
|
||||
set_progress "90" "Done"
|
||||
|
||||
@@ -16,15 +16,15 @@ existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/10
|
||||
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||
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 '{ printf "%.0f", $3 }')
|
||||
readonly disk_size=$((disk_size_gb * 1024))
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
|
||||
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
|
||||
readonly disk_size=$((${disk_size_bytes}/1024/1024))
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code, data and tmp
|
||||
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||
|
||||
echo "Disk device: ${disk_device}"
|
||||
echo "Physical memory: ${physical_memory}"
|
||||
echo "Estimated app count: ${app_count}"
|
||||
echo "Disk size: ${disk_size}"
|
||||
echo "Disk size: ${disk_size}M"
|
||||
|
||||
# Allocate swap for general app usage
|
||||
if [[ ! -f "${APPS_SWAP_FILE}" && ${swap_size} -gt 0 ]]; then
|
||||
@@ -38,6 +38,7 @@ else
|
||||
echo "Apps Swap file already exists"
|
||||
fi
|
||||
|
||||
# see start.sh for the initial default size of 8gb. On small disks the calculation might be lower than 8gb resulting in a failure to resize here.
|
||||
echo "Resizing data volume"
|
||||
home_data_size=$((disk_size - system_size - swap_size - ext4_reserved))
|
||||
echo "Resizing up btrfs user data to size ${home_data_size}M"
|
||||
@@ -5,8 +5,12 @@ map $http_upgrade $connection_upgrade {
|
||||
}
|
||||
|
||||
server {
|
||||
<% if (vhost) { %>
|
||||
listen 443;
|
||||
server_name <%= vhost %>;
|
||||
<% } else { %>
|
||||
listen 443 default_server;
|
||||
<% } %>
|
||||
|
||||
ssl on;
|
||||
# paths are relative to prefix and not to this file
|
||||
@@ -21,8 +25,10 @@ server {
|
||||
# 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 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
|
||||
# ciphers according to https://weakdh.org/sysadmin.html
|
||||
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
|
||||
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
|
||||
add_header Strict-Transport-Security "max-age=15768000";
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
|
||||
add_header X-Frame-Options "<%= xFrameOptions %>";
|
||||
|
||||
@@ -57,35 +57,6 @@ http {
|
||||
}
|
||||
}
|
||||
|
||||
# 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;
|
||||
|
||||
error_page 404 = @fallback;
|
||||
location @fallback {
|
||||
internal;
|
||||
root /home/yellowtent/box/webadmin/dist;
|
||||
rewrite ^/$ /nakeddomain.html break;
|
||||
}
|
||||
|
||||
location / {
|
||||
internal;
|
||||
root /home/yellowtent/box/webadmin/dist;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
include applications/*.conf;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ OnFailure=crashnotifier@%n.service
|
||||
StopWhenUnneeded=true
|
||||
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||
BindsTo=systemd-journald.service
|
||||
After=mysql.service nginx.service
|
||||
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
|
||||
Wants=cloudron-resize-fs.service
|
||||
|
||||
[Service]
|
||||
Type=idle
|
||||
@@ -0,0 +1,16 @@
|
||||
# Allocate swap files
|
||||
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
|
||||
# On ubuntu ec2 we use cloud-init https://wiki.archlinux.org/index.php/Cloud-init
|
||||
|
||||
[Unit]
|
||||
Description=Cloudron FS Resizer
|
||||
Before=docker.service collectd.service mysql.service sshd.service nginx.service
|
||||
After=cloud-init.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="/home/yellowtent/box/setup/start/cloudron-resize-fs.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=IPTables Restore
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,14 @@
|
||||
# The default ubuntu unbound service uses SysV fallback mode, we want a proper unit file so unbound gets restarted correctly
|
||||
|
||||
[Unit]
|
||||
Description=Unbound DNS Resolver
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
PIDFile=/run/unbound.pid
|
||||
ExecStart=/usr/sbin/unbound -d
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+2
-9
@@ -112,12 +112,6 @@ var KNOWN_ADDONS = {
|
||||
teardown: teardownSimpleAuth,
|
||||
backup: NOOP,
|
||||
restore: setupSimpleAuth
|
||||
},
|
||||
_docker: {
|
||||
setup: NOOP,
|
||||
teardown: NOOP,
|
||||
backup: NOOP,
|
||||
restore: NOOP
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,7 +213,6 @@ function getBindsSync(app, addons) {
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case '_docker': binds.push('/var/run/docker.sock:/var/run/docker.sock:rw'); break;
|
||||
case 'localstorage': binds.push(path.join(paths.DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
|
||||
default: break;
|
||||
}
|
||||
@@ -287,7 +280,7 @@ function teardownOauth(app, options, callback) {
|
||||
debugApp(app, 'teardownOauth');
|
||||
|
||||
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
|
||||
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
|
||||
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'oauth', callback);
|
||||
});
|
||||
@@ -332,7 +325,7 @@ function teardownSimpleAuth(app, options, callback) {
|
||||
debugApp(app, 'teardownSimpleAuth');
|
||||
|
||||
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
|
||||
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
|
||||
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
|
||||
|
||||
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
|
||||
});
|
||||
|
||||
+23
-7
@@ -1,5 +1,3 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
@@ -54,13 +52,14 @@ var assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
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.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
|
||||
'apps.xFrameOptions', 'apps.sso' ].join(',');
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -98,6 +97,10 @@ function postProcess(result) {
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
delete result.debugModeJson;
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -185,11 +188,12 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
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 debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
|
||||
var queries = [ ];
|
||||
var queries = [];
|
||||
queries.push({
|
||||
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 ]
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
|
||||
});
|
||||
|
||||
Object.keys(portBindings).forEach(function (env) {
|
||||
@@ -199,6 +203,14 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)',
|
||||
args: [ data.mailboxName, id, mailboxdb.TYPE_APP ]
|
||||
});
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
@@ -239,13 +251,14 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -299,6 +312,9 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
} else if (p === 'accessRestriction') {
|
||||
fields.push('accessRestrictionJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p === 'debugMode') {
|
||||
fields.push('debugModeJson = ?');
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
@@ -50,7 +50,7 @@ function setHealth(app, health, callback) {
|
||||
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
if (app.appStoreId !== '') mailer.appDied(app); // do not send mails for dev apps
|
||||
if (app.debugMode) mailer.appDied(app); // do not send mails for dev apps
|
||||
gHealthInfo[app.id].emailSent = true;
|
||||
} else {
|
||||
debugApp(app, 'waiting for sometime to update the app health');
|
||||
@@ -93,7 +93,7 @@ function checkAppHealth(app, callback) {
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
@@ -111,13 +111,13 @@ function checkAppHealth(app, callback) {
|
||||
}
|
||||
|
||||
function processApps(callback) {
|
||||
appdb.getAll(function (error, apps) {
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(apps, checkAppHealth, function (error) {
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = apps
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
@@ -138,7 +138,7 @@ function run() {
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.9.0 /bin/bash
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
@@ -166,8 +166,8 @@ function processDockerEvents() {
|
||||
debug('OOM Context: %s', context);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if ((!app || app.appStoreId !== '') && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
|
||||
mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
|
||||
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
|
||||
mailer.oomEvent(program, context); // app can be null if it's an addon crash
|
||||
lastOomMailTime = now;
|
||||
}
|
||||
});
|
||||
|
||||
+91
-66
@@ -129,18 +129,21 @@ function validateHostname(location, fqdn) {
|
||||
|
||||
// validate the port bindings
|
||||
function validatePortBindings(portBindings, tcpPorts) {
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
|
||||
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
|
||||
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
|
||||
// for custom tcp ports
|
||||
var RESERVED_PORTS = [
|
||||
22, /* ssh */
|
||||
25, /* smtp */
|
||||
53, /* dns */
|
||||
80, /* http */
|
||||
143, /* imap */
|
||||
202, /* caas ssh */
|
||||
443, /* https */
|
||||
465, /* smtps */
|
||||
587, /* submission */
|
||||
919, /* ssh */
|
||||
993, /* imaps */
|
||||
2003, /* graphite (lo) */
|
||||
2004, /* graphite (lo) */
|
||||
@@ -162,9 +165,9 @@ function validatePortBindings(portBindings, tcpPorts) {
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
|
||||
|
||||
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
|
||||
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is out of range');
|
||||
|
||||
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
|
||||
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
|
||||
|
||||
}
|
||||
|
||||
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
|
||||
@@ -207,6 +210,9 @@ function validateMemoryLimit(manifest, memoryLimit) {
|
||||
// this is needed so an app update can change the value in the manifest, and if not set by the user, the new value should be used
|
||||
if (memoryLimit === 0) return null;
|
||||
|
||||
// a special value that indicates unlimited memory
|
||||
if (memoryLimit === -1) return null;
|
||||
|
||||
if (memoryLimit < min) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too small');
|
||||
if (memoryLimit > max) return new AppsError(AppsError.BAD_FIELD, 'memoryLimit too large');
|
||||
|
||||
@@ -227,6 +233,16 @@ function validateXFrameOptions(xFrameOptions) {
|
||||
return (uri.protocol === 'http:' || uri.protocol === 'https:') ? null : new AppsError(AppsError.BAD_FIELD, 'xFrameOptions ALLOW-FROM uri must be a valid http[s] uri' );
|
||||
}
|
||||
|
||||
function validateDebugMode(debugMode) {
|
||||
assert.strictEqual(typeof debugMode, 'object');
|
||||
|
||||
if (debugMode === null) return null;
|
||||
if ('cmd' in debugMode && debugMode.cmd !== null && !Array.isArray(debugMode.cmd)) return new AppsError(AppsError.BAD_FIELD, 'debugMode.cmd must be an array or null' );
|
||||
if ('readonlyRootfs' in debugMode && typeof debugMode.readonlyRootfs !== 'boolean') return new AppsError(AppsError.BAD_FIELD, 'debugMode.readonlyRootfs must be a boolean' );
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
@@ -234,7 +250,7 @@ function getDuplicateErrorDetails(location, portBindings, error) {
|
||||
|
||||
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
|
||||
if (!match) {
|
||||
console.error('Unexpected SQL error message.', error);
|
||||
debug('Unexpected SQL error message.', error);
|
||||
return new AppsError(AppsError.INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
@@ -262,7 +278,7 @@ function getAppConfig(app) {
|
||||
}
|
||||
|
||||
function getIconUrlSync(app) {
|
||||
var iconPath = paths.APPICONS_DIR + '/' + app.id + '.png';
|
||||
var iconPath = paths.APP_ICONS_DIR + '/' + app.id + '.png';
|
||||
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
|
||||
}
|
||||
|
||||
@@ -280,11 +296,9 @@ function hasAccessTo(app, user, callback) {
|
||||
if (!app.accessRestriction.groups) return callback(null, false);
|
||||
|
||||
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
|
||||
groups.isMember(groupId, user.id, function (error, member) {
|
||||
iteratorDone(!error && member); // async.some does not take error argument in callback
|
||||
});
|
||||
}, function (result) {
|
||||
callback(null, result);
|
||||
groups.isMember(groupId, user.id, iteratorDone);
|
||||
}, function (error, result) {
|
||||
callback(null, !error && result);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -344,11 +358,9 @@ function getAllByUser(user, callback) {
|
||||
getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.filter(result, function (app, callback) {
|
||||
hasAccessTo(app, user, function (error, hasAccess) {
|
||||
callback(hasAccess);
|
||||
});
|
||||
}, callback.bind(null, null)); // never error
|
||||
async.filter(result, function (app, iteratorDone) {
|
||||
hasAccessTo(app, user, iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,6 +382,7 @@ function purchase(appId, appstoreId, callback) {
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -411,10 +424,13 @@ function unpurchase(appId, appstoreId, callback) {
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppsError(AppsError.BILLING_REQUIRED));
|
||||
if (result.statusCode !== 204) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
@@ -475,7 +491,8 @@ function install(data, auditSource, callback) {
|
||||
memoryLimit = data.memoryLimit || 0,
|
||||
altDomain = data.altDomain || null,
|
||||
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
|
||||
sso = 'sso' in data ? data.sso : null;
|
||||
sso = 'sso' in data ? data.sso : null,
|
||||
debugMode = data.debugMode || null;
|
||||
|
||||
assert(data.appStoreId || data.manifest); // atleast one of them is required
|
||||
|
||||
@@ -503,6 +520,9 @@ function install(data, auditSource, callback) {
|
||||
error = validateXFrameOptions(xFrameOptions);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateDebugMode(debugMode);
|
||||
if (error) return callback(error);
|
||||
|
||||
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['simpleauth'] || !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
@@ -514,7 +534,7 @@ function install(data, auditSource, callback) {
|
||||
if (icon) {
|
||||
if (!validator.isBase64(icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
}
|
||||
@@ -532,30 +552,26 @@ function install(data, auditSource, callback) {
|
||||
memoryLimit: memoryLimit,
|
||||
altDomain: altDomain,
|
||||
xFrameOptions: xFrameOptions,
|
||||
sso: sso
|
||||
sso: sso,
|
||||
debugMode: debugMode,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
|
||||
};
|
||||
|
||||
var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
mailboxdb.add(from, appId, mailboxdb.TYPE_APP, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'Mailbox already exists'));
|
||||
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
// save cert to boxdata/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
|
||||
// save cert to data/box/certs
|
||||
if (cert && key) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
|
||||
}
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
|
||||
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
callback(null, { id : appId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -611,7 +627,13 @@ function configure(appId, data, auditSource, callback) {
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
// save cert to data/box/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('debugMode' in data) {
|
||||
values.debugMode = data.debugMode;
|
||||
error = validateDebugMode(values.debugMode);
|
||||
if (error) return callback(error);
|
||||
}
|
||||
|
||||
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
|
||||
if ('cert' in data && 'key' in data) {
|
||||
if (data.cert && data.key) {
|
||||
error = certificates.validateCertificate(data.cert, data.key, config.appFqdn(location));
|
||||
@@ -629,16 +651,24 @@ function configure(appId, data, auditSource, callback) {
|
||||
|
||||
debug('Will configure app with id:%s values:%j', appId, values);
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
var oldName = (app.location ? app.location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
var newName = (location ? location : app.manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
mailboxdb.updateName(oldName, newName, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.restartAppTask(appId);
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
||||
taskmanager.restartAppTask(appId);
|
||||
|
||||
callback(null);
|
||||
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -674,11 +704,11 @@ function update(appId, data, auditSource, callback) {
|
||||
if (data.icon) {
|
||||
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
|
||||
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
|
||||
}
|
||||
} else {
|
||||
safe.fs.unlinkSync(path.join(paths.APPICONS_DIR, appId + '.png'));
|
||||
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,12 +720,16 @@ function update(appId, data, auditSource, callback) {
|
||||
// this allows cloudron install -f --app <appid> for an app installed from the appStore
|
||||
if (app.manifest.id !== values.manifest.id) {
|
||||
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'manifest id does not match. force to override'));
|
||||
// clear appStoreId so that this app does not get updates anymore. this will mark it as a dev app
|
||||
// clear appStoreId so that this app does not get updates anymore
|
||||
values.appStoreId = '';
|
||||
}
|
||||
|
||||
// do not update apps in debug mode
|
||||
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
|
||||
|
||||
// Ensure we update the memory limit in case the new app requires more memory as a minimum
|
||||
if (values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
|
||||
// 0 and -1 are special values for memory limit indicating unset and unlimited
|
||||
if (app.memoryLimit > 0 && values.manifest.memoryLimit && app.memoryLimit < values.manifest.memoryLimit) {
|
||||
values.memoryLimit = values.manifest.memoryLimit;
|
||||
}
|
||||
|
||||
@@ -858,24 +892,19 @@ function clone(appId, data, auditSource, callback) {
|
||||
accessRestriction: app.accessRestriction,
|
||||
xFrameOptions: app.xFrameOptions,
|
||||
lastBackupId: backupId,
|
||||
sso: !!app.sso
|
||||
sso: !!app.sso,
|
||||
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
|
||||
};
|
||||
|
||||
var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
mailboxdb.add(from, newAppId, mailboxdb.TYPE_APP, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'Mailbox already exists'));
|
||||
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
|
||||
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
|
||||
taskmanager.restartAppTask(newAppId);
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
|
||||
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
callback(null, { id : newAppId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -895,18 +924,14 @@ function uninstall(appId, auditSource, callback) {
|
||||
unpurchase(appId, result.appStoreId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mailboxdb.delByOwnerId(appId, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
taskmanager.stopAppTask(appId, function () {
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
taskmanager.stopAppTask(appId, function () {
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
|
||||
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
taskmanager.startAppTask(appId, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+26
-40
@@ -22,9 +22,8 @@ exports = module.exports = {
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs() {
|
||||
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||
return arguments;
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
@@ -34,8 +33,6 @@ var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
backups = require('./backups.js'),
|
||||
certificates = require('./certificates.js'),
|
||||
clients = require('./clients.js'),
|
||||
ClientsError = clients.ClientsError,
|
||||
config = require('./config.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:apptask'),
|
||||
@@ -54,7 +51,6 @@ var addons = require('./addons.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
_ = require('underscore');
|
||||
|
||||
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
@@ -194,6 +190,9 @@ function downloadIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// nothing to download if we dont have an appStoreId
|
||||
if (!app.appStoreId) return callback(null);
|
||||
|
||||
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
|
||||
|
||||
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
|
||||
@@ -207,22 +206,21 @@ function downloadIcon(app, callback) {
|
||||
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 retryCallback(new Error('Error saving icon:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'), res.body)) return retryCallback(new Error('Error saving icon:' + safe.error.message));
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function registerSubdomain(app, callback) {
|
||||
function registerSubdomain(app, overwrite, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof overwrite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// even though the bare domain is already registered in the appstore, we still
|
||||
// need to register it so that we have a dnsRecordId to wait for it to complete
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain location [%s]', app.location);
|
||||
|
||||
@@ -232,7 +230,7 @@ function registerSubdomain(app, callback) {
|
||||
|
||||
// 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'));
|
||||
if (config.isCustomDomain() && values.length !== 0 && !overwrite) 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
|
||||
@@ -259,7 +257,7 @@ function unregisterSubdomain(app, location, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
@@ -282,7 +280,7 @@ function removeIcon(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
|
||||
fs.unlink(path.join(paths.APP_ICONS_DIR, app.id + '.png'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
|
||||
callback(null);
|
||||
});
|
||||
@@ -297,25 +295,10 @@ function waitForDnsPropagation(app, callback) {
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
async.retry({ interval: 5000, times: 120 }, function checkStatus(retryCallback) {
|
||||
subdomains.status(app.dnsRecordId, function (error, result) {
|
||||
if (error) return retryCallback(new Error('Failed to get dns record status : ' + error.message));
|
||||
|
||||
debugApp(app, 'waitForDnsPropagation: dnsRecordId:%s status:%s', app.dnsRecordId, result);
|
||||
|
||||
if (result !== 'done') return retryCallback(new Error(util.format('app:%s not ready yet: %s', app.id, result)));
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
sysinfo.getPublicIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// finally validate with waitForDns
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 30 }, callback);
|
||||
});
|
||||
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -324,7 +307,7 @@ function waitForAltDomainDnsPropagation(app, callback) {
|
||||
|
||||
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
|
||||
// an app has an external domain and cloudron is migrated to custom domain.
|
||||
waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
|
||||
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -372,7 +355,6 @@ function install(app, callback) {
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app, app.location),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
@@ -380,7 +362,7 @@ function install(app, callback) {
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
registerSubdomain.bind(null, app, false /* overwrite */),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
@@ -428,7 +410,7 @@ function backup(app, callback) {
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
|
||||
backups.backupApp.bind(null, app, app.manifest),
|
||||
backups.backupApp.bind(null, app, app.manifest, 'appbackups' /* tag */),
|
||||
|
||||
// done!
|
||||
function (callback) {
|
||||
@@ -472,7 +454,6 @@ function restore(app, callback) {
|
||||
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
},
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
@@ -480,7 +461,7 @@ function restore(app, callback) {
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '55, Registering subdomain' }), // ip might change during upgrades
|
||||
registerSubdomain.bind(null, app),
|
||||
registerSubdomain.bind(null, app, true /* overwrite */),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '60, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
@@ -543,7 +524,13 @@ function configure(app, callback) {
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
|
||||
registerSubdomain.bind(null, app),
|
||||
registerSubdomain.bind(null, app, true /* overwrite */),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
|
||||
docker.downloadImage.bind(null, app.manifest),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
|
||||
createVolume.bind(null, app),
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
|
||||
@@ -611,14 +598,13 @@ function update(app, callback) {
|
||||
|
||||
docker.deleteImage(app.oldConfig.manifest, done);
|
||||
},
|
||||
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
|
||||
|
||||
function (next) {
|
||||
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
|
||||
backups.backupApp.bind(null, app, app.oldConfig.manifest)
|
||||
backups.backupApp.bind(null, app, app.oldConfig.manifest, 'appbackups' /* tag */)
|
||||
], next);
|
||||
},
|
||||
|
||||
|
||||
+2
-1
@@ -49,8 +49,9 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// box versions (0.93.x and below) used to use appbackup_ prefix
|
||||
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) {
|
||||
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
+34
-51
@@ -46,8 +46,7 @@ var addons = require('./addons.js'),
|
||||
shell = require('./shell.js'),
|
||||
settings = require('./settings.js'),
|
||||
SettingsError = require('./settings.js').SettingsError,
|
||||
util = require('util'),
|
||||
webhooks = require('./webhooks.js');
|
||||
util = require('util');
|
||||
|
||||
var BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
||||
BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||
@@ -132,7 +131,6 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -149,7 +147,6 @@ function getRestoreConfig(backupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -174,15 +171,16 @@ function getRestoreUrl(backupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function copyLastBackup(app, manifest, callback) {
|
||||
function copyLastBackup(app, manifest, prefix, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof prefix, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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);
|
||||
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var toFilenameArchive = util.format('%s/app_%s_%s_v%s.tar.gz', prefix, app.id, timestamp, manifest.version);
|
||||
var toFilenameConfig = util.format('%s/app_%s_%s_v%s.json', prefix, app.id, timestamp, manifest.version);
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||
@@ -206,11 +204,12 @@ function copyLastBackup(app, manifest, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
function backupBoxWithAppBackupIds(appBackupIds, prefix, callback) {
|
||||
assert(util.isArray(appBackupIds));
|
||||
assert.strictEqual(typeof prefix, 'string');
|
||||
|
||||
var now = new Date();
|
||||
var filebase = util.format('backup_%s-v%s', now.toISOString(), config.version());
|
||||
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var filebase = util.format('%s/box_%s_v%s', prefix, timestamp, config.version());
|
||||
var filename = filebase + '.tar.gz';
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
@@ -229,7 +228,7 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
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) {
|
||||
api(backupConfig.provider).backupDone(filename, null /* app */, appBackupIds, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, filename);
|
||||
});
|
||||
@@ -239,18 +238,6 @@ function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -260,29 +247,14 @@ function canBackupApp(app) {
|
||||
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) {
|
||||
function createNewAppBackup(app, manifest, prefix, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof prefix, 'string');
|
||||
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 timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
var filebase = util.format('%s/app_%s_%s_v%s', prefix, app.id, timestamp, manifest.version);
|
||||
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
|
||||
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
@@ -324,9 +296,10 @@ function setRestorePoint(appId, lastBackupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function backupApp(app, manifest, callback) {
|
||||
function backupApp(app, manifest, prefix, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof prefix, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var backupFunction;
|
||||
@@ -337,11 +310,13 @@ function backupApp(app, manifest, callback) {
|
||||
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
|
||||
}
|
||||
|
||||
backupFunction = reuseOldAppBackup.bind(null, app, manifest);
|
||||
// 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
|
||||
backupFunction = copyLastBackup.bind(null, app, manifest, prefix);
|
||||
} else {
|
||||
var appConfig = apps.getAppConfig(app);
|
||||
appConfig.manifest = manifest;
|
||||
backupFunction = createNewAppBackup.bind(null, app, manifest);
|
||||
backupFunction = createNewAppBackup.bind(null, app, manifest, prefix);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
@@ -367,6 +342,8 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var prefix = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
||||
|
||||
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
|
||||
|
||||
apps.getAll(function (error, allApps) {
|
||||
@@ -375,18 +352,20 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
var processed = 0;
|
||||
var step = 100/(allApps.length+1);
|
||||
|
||||
progress.set(progress.BACKUP, processed, '');
|
||||
progress.set(progress.BACKUP, step * processed, '');
|
||||
|
||||
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
|
||||
++processed;
|
||||
|
||||
backupApp(app, app.manifest, function (error, backupId) {
|
||||
backupApp(app, app.manifest, prefix, 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);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location)));
|
||||
|
||||
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||
});
|
||||
@@ -398,7 +377,9 @@ function backupBoxAndApps(auditSource, callback) {
|
||||
|
||||
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, step * processed, 'Backing up system data');
|
||||
|
||||
backupBoxWithAppBackupIds(backupIds, prefix, 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 });
|
||||
@@ -421,7 +402,7 @@ function backup(auditSource, callback) {
|
||||
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
|
||||
if (error) {
|
||||
debug('backup failed.', error);
|
||||
mailer.backupFailed(JSON.stringify(error));
|
||||
mailer.backupFailed(error);
|
||||
}
|
||||
|
||||
locker.unlock(locker.OP_FULL_BACKUP);
|
||||
@@ -433,6 +414,8 @@ function backup(auditSource, callback) {
|
||||
function ensureBackup(auditSource, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
debug('ensureBackup: %j', auditSource);
|
||||
|
||||
getPaged(1, 1, function (error, backups) {
|
||||
if (error) {
|
||||
debug('Unable to list backups', error);
|
||||
|
||||
+26
-17
@@ -4,13 +4,13 @@ var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
parseLinks = require('parse-links'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
ursa = require('ursa'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -81,23 +81,33 @@ function b64(str) {
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
return Buffer.from(match[1], 'hex');
|
||||
}
|
||||
|
||||
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||
|
||||
var that = this;
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(privateKey.getExponent()),
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
this.getNonce(function (error, nonce) {
|
||||
@@ -107,9 +117,9 @@ Acme.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = ursa.createSigner('sha256');
|
||||
var signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(privateKey, 'base64'));
|
||||
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
|
||||
|
||||
var data = {
|
||||
header: header,
|
||||
@@ -207,12 +217,11 @@ Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
var token = challenge.token;
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||
|
||||
var jwk = {
|
||||
e: b64(privateKey.getExponent()),
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(privateKey.getModulus())
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
};
|
||||
|
||||
var shasum = crypto.createHash('sha256');
|
||||
@@ -318,7 +327,6 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var csrFile = path.join(outdir, domain + '.csr');
|
||||
var privateKeyFile = path.join(outdir, domain + '.key');
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
if (safe.fs.existsSync(privateKeyFile)) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
@@ -344,12 +352,16 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
Acme.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: linkHeader %s', linkHeader);
|
||||
|
||||
var linkInfo = parseLinks(linkHeader);
|
||||
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
|
||||
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
|
||||
|
||||
superagent.get(this.caOrigin + linkInfo.up).buffer().parse(function (res, done) {
|
||||
debug('downloadChain: downloading from %s', intermediateCertUrl);
|
||||
|
||||
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
@@ -358,8 +370,6 @@ Acme.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var chainDer = result.text;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
|
||||
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
@@ -385,7 +395,6 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
var execSync = safe.child_process.execSync;
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
|
||||
debug('downloadCertificate: cert der file for %s saved', domain);
|
||||
|
||||
+123
-50
@@ -1,14 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
installAdminCertificate: installAdminCertificate,
|
||||
renewAll: renewAll,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
CertificatesError: CertificatesError,
|
||||
|
||||
ensureFallbackCertificate: ensureFallbackCertificate,
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
|
||||
validateCertificate: validateCertificate,
|
||||
ensureCertificate: ensureCertificate,
|
||||
getAdminCertificatePath: getAdminCertificatePath,
|
||||
|
||||
setAdminCertificate: setAdminCertificate,
|
||||
getAdminCertificate: getAdminCertificate,
|
||||
|
||||
renewAll: renewAll,
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
|
||||
events: null,
|
||||
|
||||
EVENT_CERT_CHANGED: 'cert_changed',
|
||||
|
||||
// exported for testing
|
||||
_getApi: getApi
|
||||
@@ -31,11 +42,8 @@ var acme = require('./cert/acme.js'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
user = require('./user.js'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js'),
|
||||
x509 = require('x509');
|
||||
util = require('util');
|
||||
|
||||
function CertificatesError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
@@ -60,6 +68,20 @@ CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||
CertificatesError.NOT_FOUND = 'Not Found';
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = new (require('events').EventEmitter)();
|
||||
callback();
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
exports.events = null;
|
||||
callback();
|
||||
}
|
||||
|
||||
function getApi(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -84,38 +106,53 @@ function getApi(app, callback) {
|
||||
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
user.getOwner(function (error, owner) {
|
||||
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
|
||||
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
|
||||
|
||||
callback(null, api, options);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function installAdminCertificate(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
function ensureFallbackCertificate(callback) {
|
||||
// ensure a fallback certificate that much of our code requires
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
|
||||
|
||||
settings.getTlsConfig(function (error, tlsConfig) {
|
||||
if (error) return callback(error);
|
||||
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
|
||||
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
|
||||
|
||||
if (tlsConfig.provider === 'caas') return callback();
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
|
||||
debug('ensureFallbackCertificate: using fallback certs provided by user');
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
sysinfo.getIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
return callback();
|
||||
}
|
||||
|
||||
waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
|
||||
if (error) return callback(error);
|
||||
if (config.tlsCert() && config.tlsKey()) {
|
||||
// cert from CaaS or cloudron-setup. these files should _not_ be part of the backup
|
||||
debug('ensureFallbackCertificate: using CaaS/cloudron-setup fallback certs');
|
||||
if (!safe.fs.writeFileSync(fallbackCertPath, config.tlsCert())) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(fallbackKeyPath, config.tlsKey())) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
|
||||
if (error) { // currently, this can never happen
|
||||
debug('Error obtaining certificate. Proceed anyway', error);
|
||||
return callback();
|
||||
}
|
||||
return callback();
|
||||
}
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
|
||||
// FIXME: this cert does not cover the naked domain. needs SAN
|
||||
if (config.fqdn()) {
|
||||
debug('ensureFallbackCertificate: generating self-signed certificate');
|
||||
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
|
||||
safe.child_process.execSync(certCommand);
|
||||
|
||||
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
return callback();
|
||||
} else {
|
||||
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
|
||||
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
|
||||
}
|
||||
}
|
||||
|
||||
function isExpiringSync(certFilePath, hours) {
|
||||
@@ -203,12 +240,14 @@ function renewAll(auditSource, callback) {
|
||||
|
||||
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
||||
var configureFunc = app.location === constants.ADMIN_LOCATION ?
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
|
||||
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
|
||||
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||
|
||||
configureFunc(function (ignoredError) {
|
||||
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
|
||||
|
||||
iteratorCallback(); // move to next app
|
||||
});
|
||||
});
|
||||
@@ -228,25 +267,30 @@ function validateCertificate(cert, key, fqdn) {
|
||||
if (!cert && key) return new Error('missing cert');
|
||||
if (cert && !key) return new Error('missing key');
|
||||
|
||||
var content;
|
||||
try {
|
||||
content = x509.parseCert(cert);
|
||||
} catch (e) {
|
||||
return new Error('invalid cert: ' + e.message);
|
||||
}
|
||||
|
||||
// check expiration
|
||||
if (content.notAfter < new Date()) return new Error('cert expired');
|
||||
|
||||
function matchesDomain(domain) {
|
||||
if (typeof domain !== 'string') return false;
|
||||
if (domain === fqdn) return true;
|
||||
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check domain
|
||||
var domains = content.altNames.concat(content.subject.commonName);
|
||||
// get commonName (http://stackoverflow.com/questions/17353122/parsing-strings-crt-files)
|
||||
var result = safe.child_process.execSync('openssl x509 -noout -subject | sed -r "s|.*CN=(.*)|\\1|; s|/[^/]*=.*$||"', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error(util.format('could not get CN'));
|
||||
var commonName = result.trim();
|
||||
debug('validateCertificate: detected commonName as %s', commonName);
|
||||
|
||||
// https://github.com/drwetter/testssl.sh/pull/383
|
||||
var cmd = `openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
|
||||
grep "DNS:" | \
|
||||
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"`;
|
||||
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
|
||||
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
|
||||
debug('validateCertificate: detected altNames as %j', altNames);
|
||||
|
||||
// check altNames
|
||||
var domains = altNames.concat(commonName);
|
||||
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
||||
|
||||
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||
@@ -254,6 +298,10 @@ function validateCertificate(cert, key, fqdn) {
|
||||
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
||||
|
||||
// check expiration
|
||||
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
|
||||
if (!result) return new Error('cert expired');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -273,6 +321,8 @@ function setFallbackCertificate(cert, key, callback) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + config.fqdn());
|
||||
|
||||
nginx.reload(function (error) {
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||
|
||||
@@ -287,15 +337,14 @@ function getFallbackCertificatePath(callback) {
|
||||
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||
}
|
||||
|
||||
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
|
||||
function setAdminCertificate(cert, key, callback) {
|
||||
assert.strictEqual(typeof cert, 'string');
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
var error = validateCertificate(cert, key, vhost);
|
||||
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||
@@ -304,21 +353,44 @@ function setAdminCertificate(cert, key, callback) {
|
||||
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
|
||||
|
||||
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
|
||||
}
|
||||
|
||||
function getAdminCertificatePath(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var vhost = config.adminFqdn();
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
|
||||
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||
keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||
|
||||
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||
|
||||
getFallbackCertificatePath(callback);
|
||||
}
|
||||
|
||||
function getAdminCertificate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var cert = safe.fs.readFileSync(certFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
var key = safe.fs.readFileSync(keyFilePath);
|
||||
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
|
||||
|
||||
return callback(null, cert, key);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCertificate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -340,10 +412,11 @@ function ensureCertificate(app, callback) {
|
||||
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
|
||||
|
||||
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
} else {
|
||||
debug('ensureCertificate: %s cert does not exist', domain);
|
||||
}
|
||||
|
||||
debug('ensureCertificate: %s cert require renewal', domain);
|
||||
|
||||
getApi(app, function (error, api, apiOptions) {
|
||||
if (error) return callback(error);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user