mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
Compare commits
1165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b339ea062 | |||
| 236ef03dbd | |||
| 53cc561048 | |||
| bb4b143f3b | |||
| 3af41cd37d | |||
| 7e32a17742 | |||
| 6c265534a4 | |||
| 1d42133d44 | |||
| df911c9b9e | |||
| a6f40dd574 | |||
| 688215e787 | |||
| 1cfa2e04bc | |||
| b4f6901903 | |||
| 0149382cdc | |||
| 697c9973a7 | |||
| 788fd3df81 | |||
| 996cac5fed | |||
| 0a8b78cb84 | |||
| b4eb74f5ff | |||
| 57d1f31054 | |||
| 9f02f51b87 | |||
| 911a17b149 | |||
| 3d969bd2b4 | |||
| f800cee4fa | |||
| b49fc33cb3 | |||
| 00e235a1ee | |||
| 37a6b2da67 | |||
| 913e977c8d | |||
| c2ddcb3b40 | |||
| ab9544c0d3 | |||
| 4bfe849409 | |||
| 3bdb92fcad | |||
| cf9e3069f2 | |||
| ed0cbfb31e | |||
| 32b2f6117e | |||
| ae92521310 | |||
| 5802df4365 | |||
| c1901f4e12 | |||
| 8d98282afd | |||
| dd839bf295 | |||
| 3af6073576 | |||
| 2518af5f9e | |||
| 7b793d84c8 | |||
| af9bc7ea7d | |||
| ac055b09e9 | |||
| df42914da6 | |||
| 2471bdda00 | |||
| c7e01b139d | |||
| 9d80ff5a05 | |||
| 39b3941892 | |||
| b311abed31 | |||
| ce667ddae0 | |||
| 0fee993a4b | |||
| 0ea5c9d8e8 | |||
| 63ac260bd5 | |||
| a01a39b153 | |||
| f9a4ad7904 | |||
| e60b67d246 | |||
| 9004d69c6f | |||
| 8856a2d77b | |||
| 54a061bdda | |||
| 65b4b100a8 | |||
| 7cc9286e0f | |||
| 2f25639ea0 | |||
| 2070c215a2 | |||
| 94b98222c2 | |||
| 9c605ad153 | |||
| b7c7e59dac | |||
| 767c1436d3 | |||
| 699cf6ff45 | |||
| 9201870f6c | |||
| 6722f58668 | |||
| 7b9b7cef11 | |||
| 7d4fce09dc | |||
| 2075501d86 | |||
| bd099f5e71 | |||
| baf257adcb | |||
| 4fd1986885 | |||
| e1afac9439 | |||
| 580d9db85e | |||
| 42e2fd35d8 | |||
| 1a40c7c27c | |||
| f3bec41eb9 | |||
| 825634d24e | |||
| cb097e6b0a | |||
| 1cfb03fb74 | |||
| f293df647c | |||
| 10522438b7 | |||
| e2e5bd6f19 | |||
| cd7a0a9757 | |||
| b3eda248a3 | |||
| 95b51c48be | |||
| 486888f595 | |||
| 17ab8145b5 | |||
| b3ebc69034 | |||
| 761dde2f1b | |||
| 2bb6a3f4d0 | |||
| e83e947ca3 | |||
| 73733a8fb9 | |||
| ce6c23a360 | |||
| 99d8e6a30f | |||
| 2fa1d8ac48 | |||
| ca7e425ce8 | |||
| 8b9a19eef1 | |||
| 7f629df4d5 | |||
| 98ddc3596c | |||
| 5d23be6242 | |||
| d15d3a524b | |||
| 1e1d9acb1b | |||
| 55ee94bed0 | |||
| d228d29944 | |||
| 013cc66d8e | |||
| c7ed6eee5e | |||
| 8082d1fed6 | |||
| d2a10dbe69 | |||
| 14645142db | |||
| f34b2ef90b | |||
| ce894665a8 | |||
| 0d00f3a55b | |||
| 48ff373ff7 | |||
| e9efee0e64 | |||
| 4b3e7aee0b | |||
| dd53b287f2 | |||
| 21526efe51 | |||
| 7413045f0e | |||
| d76c508566 | |||
| 8fb46de5e4 | |||
| af1944f28d | |||
| 214ea14f29 | |||
| 5fb420c703 | |||
| 2420f6c000 | |||
| b0d7332a0c | |||
| f71b56a5d0 | |||
| d55efc791f | |||
| f63645546d | |||
| e2dd3e3587 | |||
| 27ab780317 | |||
| e2d4d097e7 | |||
| ac8cb6ba0d | |||
| 4ce81fd07f | |||
| df9eeb7f8f | |||
| 31c4fdbf79 | |||
| 48e367ff7d | |||
| fd02492cb7 | |||
| addfa35d93 | |||
| 5afdc56796 | |||
| fb1c333a83 | |||
| c3e1da8e04 | |||
| 20a753e2e5 | |||
| 3a398775fb | |||
| 61a7434379 | |||
| 09f5e29327 | |||
| 29edb4ccfe | |||
| 197d6fb644 | |||
| d4e565e595 | |||
| 1fce2b180f | |||
| be6ccd129d | |||
| f7cecf0945 | |||
| 7b2198f7e5 | |||
| 52221db7ef | |||
| befbf48563 | |||
| 56a61bab56 | |||
| d480022711 | |||
| f1abb92f0c | |||
| 5792be71fa | |||
| c2630bb3a3 | |||
| 5e3010d455 | |||
| ccbf65c8e8 | |||
| 9d07cde385 | |||
| 464b9d7c80 | |||
| 5c81d0d89a | |||
| 62cd643868 | |||
| c0bf02b8b2 | |||
| 1b7dd70f72 | |||
| 372a08be49 | |||
| fd46a1c3b3 | |||
| 5aae7178ad | |||
| dea8220eee | |||
| a4be0b88f6 | |||
| d8101573be | |||
| 41cdb357bb | |||
| 38caddffe7 | |||
| 80fe166902 | |||
| 0e26f983d6 | |||
| fc08fcab52 | |||
| 77dc99e71d | |||
| 5041bfcb5c | |||
| 5be76856bd | |||
| 2a3f5e1ad1 | |||
| f8650a3493 | |||
| 90a52a29c5 | |||
| 8859c92f80 | |||
| 01e5632949 | |||
| 18a4276e25 | |||
| c06032f35f | |||
| 95a6b2c991 | |||
| ee28f6caaa | |||
| 30c9e50701 | |||
| 9aadd725d2 | |||
| 6cfb1cb6fd | |||
| 2dc8ac1e62 | |||
| e952e2a691 | |||
| 040ac5cad8 | |||
| 05685863e3 | |||
| d324c0a1c3 | |||
| 03f8b25b50 | |||
| b0e2c2da78 | |||
| f28a8eca91 | |||
| ca69e54cb6 | |||
| 4629abd5a2 | |||
| dc99f4a7a3 | |||
| 389ec21d0c | |||
| 9341201132 | |||
| 88dd83a365 | |||
| 74285d50c4 | |||
| 60d0611ac2 | |||
| f939222942 | |||
| c293c2e9a3 | |||
| e34ca9acd1 | |||
| 83071a3459 | |||
| edf364bf21 | |||
| 1e037883b0 | |||
| d909f167ff | |||
| 4592aaa3e2 | |||
| 95d1a12422 | |||
| 62aa42cccf | |||
| 5cffd3780a | |||
| def75ffcfe | |||
| ad8e611098 | |||
| 3ec1844e4a | |||
| 523670ba0d | |||
| 35dea24ffd | |||
| e55104a155 | |||
| 111745c564 | |||
| c7df1ffc6f | |||
| 2b7e75e079 | |||
| bcdaa09c75 | |||
| 2fc65dcb99 | |||
| 44a3b58e52 | |||
| 0a256053ee | |||
| 46de9ac03e | |||
| a53dc1d9c8 | |||
| f0462322fd | |||
| c59d2a6288 | |||
| 3e3ff2a70b | |||
| 16bc11e72e | |||
| 2719f1efaa | |||
| cff1be0ae8 | |||
| 39ac62a1a1 | |||
| f427dbbd60 | |||
| c3f689a7d9 | |||
| 85f3a9f3b0 | |||
| 13ba4b433d | |||
| 96f27a4965 | |||
| 0e502899a8 | |||
| 424b44c247 | |||
| 01a71c366d | |||
| 990fbeb3a4 | |||
| 5a9a898ba2 | |||
| fe1fbe0005 | |||
| c56a139fdc | |||
| df50eda811 | |||
| f5d3313210 | |||
| 97fcc9ff99 | |||
| 8a6b2b4447 | |||
| 757eaeae92 | |||
| b7dd61f6bc | |||
| d2a95a04a4 | |||
| 0cc993f403 | |||
| 3a64580663 | |||
| d087e28dce | |||
| 96adfaebe1 | |||
| ddf84f8257 | |||
| 507f993075 | |||
| 73a6a60785 | |||
| cf4cf58faf | |||
| 6bc3c74c0c | |||
| 598ce1e354 | |||
| 4685b76e08 | |||
| 78c9109f6c | |||
| 7e248fc0ba | |||
| c526fa9119 | |||
| 520e0fd985 | |||
| 54a7eba358 | |||
| 1494ba2e6e | |||
| a5b3548ede | |||
| 8318aa0113 | |||
| e69c42956b | |||
| 53ca589c11 | |||
| ca8ff8718e | |||
| e8e48e4c4a | |||
| 67e17ed3f8 | |||
| 2a6a40e93b | |||
| eda34423d7 | |||
| 7ce1f6e736 | |||
| 5c53620a72 | |||
| 5f94cec1e2 | |||
| 646350fa7f | |||
| e162a055cc | |||
| 28d3ad3ada | |||
| 0bd44a7764 | |||
| 8be6d887e2 | |||
| 66b14a0d32 | |||
| 153a612253 | |||
| 1a1b55e133 | |||
| 879de20edf | |||
| e77ad3f9bb | |||
| 4ce86ff5fa | |||
| e290c010e6 | |||
| 33d267fa1b | |||
| 601a744159 | |||
| f630d7c3fa | |||
| 91bfefcf8c | |||
| a1b01e6d5f | |||
| 48594617b5 | |||
| b35b9dcff7 | |||
| a3e317773a | |||
| 16431d222c | |||
| ee49a23220 | |||
| a9eef521ec | |||
| 901d33b59c | |||
| 255116fde7 | |||
| 00ebea2536 | |||
| dedf9774c7 | |||
| 6b1c62133d | |||
| d4251b2545 | |||
| b9d1698d74 | |||
| 7c696e1cb6 | |||
| 165d60421d | |||
| c7962118f8 | |||
| 892a204013 | |||
| 0e6aedc7ed | |||
| c547a4d835 | |||
| fc9668baa5 | |||
| ba17d46f15 | |||
| 54a4f93854 | |||
| bdd816488d | |||
| 36dcfee2f7 | |||
| 4d13ddf6b3 | |||
| 9e25475475 | |||
| e955aa7f2a | |||
| 81d2b54dfd | |||
| 7956ff0313 | |||
| 9ff25fb64b | |||
| 04df69f633 | |||
| 908eb57795 | |||
| ecfae074dc | |||
| be5d394e56 | |||
| 849a27ee61 | |||
| 062f3ea43a | |||
| 5cfedcfe33 | |||
| 4d2fc530d0 | |||
| 3970204009 | |||
| f046f557fa | |||
| 401958938d | |||
| 566cffe53d | |||
| 028bc2f9be | |||
| 813d9bc316 | |||
| 79ba458051 | |||
| cf220be9b5 | |||
| c433572585 | |||
| a42b576382 | |||
| fb9b53026d | |||
| 2ac54e5a7b | |||
| 8eecdc6d1f | |||
| 50577e2bd2 | |||
| 7bc1f986e8 | |||
| d796621ccc | |||
| 751e9fb7be | |||
| f6113264f4 | |||
| a3534a730b | |||
| 7f8b8a0e43 | |||
| bd6f7b6d83 | |||
| b0a4beb66a | |||
| 472c2d828c | |||
| 01ee49045e | |||
| 7bd9f821dd | |||
| b20ecc7b54 | |||
| 61eb9d4e29 | |||
| 43eb5a001c | |||
| f58692abb7 | |||
| c1760fb764 | |||
| e9bc0e7e98 | |||
| ffcadcd99e | |||
| 7a733a8d54 | |||
| ce97313fda | |||
| 7b81967a3c | |||
| ff811f594b | |||
| 0bf80b3c89 | |||
| ae3b369fe1 | |||
| 77b15e7194 | |||
| 20537f974e | |||
| 4476a64bdf | |||
| d4b701576e | |||
| 721c053712 | |||
| e3071157f0 | |||
| c07af89e48 | |||
| 9c846106fa | |||
| cf94d1f1f1 | |||
| 6187440f35 | |||
| 57b7c3494f | |||
| dda18c28c5 | |||
| f8d6eaaa96 | |||
| 47d4fabb58 | |||
| 80039f60d5 | |||
| 5a5e9b8a89 | |||
| b7ed3b77bd | |||
| 75b925c326 | |||
| 91d419ee6c | |||
| 23345098ea | |||
| 7ce91ea1a1 | |||
| 41079f1015 | |||
| 712dfa40cd | |||
| decfd6108c | |||
| b890bbfa63 | |||
| 0e3a570b85 | |||
| 7060c809c0 | |||
| 9dbfd84c5b | |||
| fce380a044 | |||
| 46ba15ab03 | |||
| 1e39ca39c3 | |||
| 80ef1ae51c | |||
| 4d0715d226 | |||
| 8a274169da | |||
| 21d8298fe1 | |||
| 5d6f6d8d5b | |||
| bacf6156c1 | |||
| 1d1b213f1f | |||
| 1f11af42f1 | |||
| a026c8748f | |||
| 9f7d89b3cd | |||
| 92a77cc78e | |||
| b0c84e3de7 | |||
| bbc914e174 | |||
| 3fca4055d2 | |||
| 66afa16aed | |||
| 9b0a8de7de | |||
| 04bbede17d | |||
| 0e3bafcc54 | |||
| b48f719b8e | |||
| 289fcbd08c | |||
| f6875bb893 | |||
| 7e803adf13 | |||
| 5b5deee5b3 | |||
| 7dae4cb685 | |||
| 27fad98179 | |||
| 58f7e3a829 | |||
| 4a15bd8ff8 | |||
| b030ef1aca | |||
| cc46a99f97 | |||
| becec6cb6b | |||
| bc33db9fc0 | |||
| 7d4579e737 | |||
| b7c90751b0 | |||
| 88fd1cba71 | |||
| e43cc316ff | |||
| e3f24a29fa | |||
| 890e526bde | |||
| 16ce455fca | |||
| 29b7164468 | |||
| acdd03f609 | |||
| 03b35ecdd0 | |||
| 5307e18085 | |||
| 2cea944cdb | |||
| c08540c7b7 | |||
| 3934700a08 | |||
| 0913eb6655 | |||
| 1bfbe354f5 | |||
| 2d78e20120 | |||
| 77210513c9 | |||
| 2e6f8bdf19 | |||
| 25144fedd5 | |||
| 27f64dd9a4 | |||
| 9d7648f02f | |||
| 1ef8babfef | |||
| 4ea7bf0510 | |||
| 5dcf1d13a9 | |||
| 94d37d05e5 | |||
| 0cbdc458c5 | |||
| c1437c7b46 | |||
| f357f65d04 | |||
| ef8e952fc4 | |||
| a2bc383e15 | |||
| 23930355a7 | |||
| bb9f41e613 | |||
| bc110d8055 | |||
| b23b19e5c3 | |||
| 65b1a4282e | |||
| 1dbb3f6f43 | |||
| af3dc25dfe | |||
| 5a0c0079a1 | |||
| af8f563ed3 | |||
| 93af4a4864 | |||
| 28f188e3ef | |||
| b29224f62f | |||
| d756da41b9 | |||
| cdab4a3b85 | |||
| b88c57ba93 | |||
| 1a5496eced | |||
| b264e6a191 | |||
| ae1b495262 | |||
| 16939ca192 | |||
| 60cd513a33 | |||
| 27d94c64ed | |||
| 21a0f857d3 | |||
| 03a6e8aee2 | |||
| d0862ddf86 | |||
| 4afbb89774 | |||
| 5ec57a9533 | |||
| f088e8960b | |||
| 27dec42ad6 | |||
| b70053090c | |||
| f10e2254ae | |||
| 1f92fc3fc0 | |||
| f71b114a84 | |||
| e3e0532613 | |||
| 2c0f121550 | |||
| 6f41cff75a | |||
| 9b39616c1b | |||
| fad3d66093 | |||
| ff99ef74c8 | |||
| 6990e73b11 | |||
| 860a1237ab | |||
| 97b5bf1fb7 | |||
| ed3418c046 | |||
| a2230868e0 | |||
| 71bab74148 | |||
| 661ea57907 | |||
| 1f18efb0ba | |||
| 8ae46bce93 | |||
| f19a414e09 | |||
| 0ee2933234 | |||
| 9890f579f8 | |||
| 2ee337ead5 | |||
| 362e14fa1a | |||
| 524fe62594 | |||
| 3c87e1e60d | |||
| 0cac868a36 | |||
| 2480c66857 | |||
| 186c477f3c | |||
| 570670be8c | |||
| 22b7226581 | |||
| f16f715b59 | |||
| 75adb787c4 | |||
| 6123377e66 | |||
| 778cccb15d | |||
| 0256dae657 | |||
| 88a93838de | |||
| 0855988427 | |||
| 84b121bbe1 | |||
| 48fb7b0dd7 | |||
| 01e550a9be | |||
| 63a2e0bab6 | |||
| 24657859a8 | |||
| 67d07e895c | |||
| d7df6bc738 | |||
| a4e1de93a7 | |||
| 41be557f0c | |||
| 9417fd933e | |||
| 067d21d0f2 | |||
| 3882da6ac5 | |||
| 77b780b8ca | |||
| 127e8bf3b6 | |||
| 74faed166a | |||
| dbd05d6e82 | |||
| b5d35c7e09 | |||
| c39eb3bacd | |||
| 57fad9148c | |||
| 0f88cdc80e | |||
| e2a9949b16 | |||
| 38e3c7a8f7 | |||
| 67f166fa02 | |||
| c7df5fb119 | |||
| a4be47d7ad | |||
| aaea94a48d | |||
| c3d9c45f58 | |||
| a2a48cc065 | |||
| cf407f7176 | |||
| d6dd17a483 | |||
| a66071099c | |||
| 9a6e569412 | |||
| 7dfa565d00 | |||
| d2e5f01542 | |||
| f4e373e0d2 | |||
| c8691db2b7 | |||
| affe51cb19 | |||
| 57118919d2 | |||
| 7db05a80dd | |||
| a8ba71edef | |||
| 45a99c3fd3 | |||
| 58e6b83e95 | |||
| f556a72fe2 | |||
| cd7a5cab8a | |||
| 67b5e0dbe8 | |||
| b68f0cbde4 | |||
| ebc3627c73 | |||
| 295730408b | |||
| 5a9f133491 | |||
| f30afa4956 | |||
| 171cedf0f0 | |||
| 27d8ef14f8 | |||
| f6d13f57bb | |||
| 5f36167f1a | |||
| 8fb4ae916c | |||
| 48da4aeee0 | |||
| 07df9eecda | |||
| 7f214a0e46 | |||
| e1a0a1e73c | |||
| 1278b0ec73 | |||
| 3e9bd931ed | |||
| 9d588319dd | |||
| 0012ca8ca5 | |||
| 288e276abe | |||
| f22e745514 | |||
| 070c31eac5 | |||
| 1a56ebea70 | |||
| 70e1cbda21 | |||
| 1ede3967c1 | |||
| 60f2df54e0 | |||
| ba708f51f2 | |||
| b106b1c131 | |||
| 0df31f63ab | |||
| 64d4da5a37 | |||
| 7aec38a73e | |||
| a2fd8caa69 | |||
| f546636c52 | |||
| 38ccc4f672 | |||
| 04e669a6be | |||
| cc3f139d1f | |||
| d50442da01 | |||
| 404b05a44c | |||
| 3d7c1ad31d | |||
| d300e775a6 | |||
| 7ee2d1c339 | |||
| 54a98773f8 | |||
| 737a3f0bad | |||
| 3bd9636a5b | |||
| 76b21de0c6 | |||
| dabb058167 | |||
| f394313fee | |||
| b7c5e45fff | |||
| 0a224654c2 | |||
| 47e4a36d7e | |||
| e420a1de4d | |||
| 62dc0f7698 | |||
| 2d31d92271 | |||
| 1981fe2072 | |||
| f68bd37acf | |||
| 76877eb6fa | |||
| 3d66d053c7 | |||
| 0d3ae3810f | |||
| 0e31cff762 | |||
| c27110e37d | |||
| 89441a22aa | |||
| 557135185c | |||
| 4d39fd4165 | |||
| f4c03e56b8 | |||
| d2b6aa9033 | |||
| 5dd40b9377 | |||
| 001b77e7e1 | |||
| 9d91d32d82 | |||
| a60ac7ca17 | |||
| 42ba0da6b0 | |||
| f527c708f2 | |||
| 6f474982ed | |||
| fd6cd52728 | |||
| c9e49f4366 | |||
| 46fd9f4a53 | |||
| 4da641533d | |||
| 79df2c7ce7 | |||
| 3e28af1723 | |||
| 866a95de38 | |||
| 6aa0574a53 | |||
| bb97eafa82 | |||
| 51d5efee1b | |||
| c980804514 | |||
| b883803b21 | |||
| 7e3a7d7044 | |||
| 9ad6012782 | |||
| 5a96cbbeaa | |||
| 416977436e | |||
| 54ec0a1308 | |||
| ebd78e983f | |||
| 1cf726348f | |||
| 41f75e6d1b | |||
| 0e3037631f | |||
| 6fbf4f96b6 | |||
| e35709a99e | |||
| f3602d7d08 | |||
| 526e10a2e0 | |||
| 0b21734571 | |||
| 499872f31d | |||
| 5cc16e098c | |||
| 364e27d5f2 | |||
| 1f4e0bd17c | |||
| 0557e18472 | |||
| cfd66ab8c3 | |||
| 691b763613 | |||
| 3ddb501190 | |||
| 997e808088 | |||
| 13441ad0f8 | |||
| aa508591c1 | |||
| 818f0201fc | |||
| 890f43ffa5 | |||
| e270ab65b3 | |||
| 926373f9c1 | |||
| 111c6177d2 | |||
| 91f72f25ab | |||
| da540ccf8c | |||
| d6396f82fe | |||
| 4fa250a6a1 | |||
| b42cfcea60 | |||
| aca6dfbd60 | |||
| 5f7e6d03ff | |||
| 88ad742da0 | |||
| a8d4042853 | |||
| 44a9339c0a | |||
| 113c7ff49a | |||
| 40dbe243d9 | |||
| d422d24278 | |||
| 109c927dad | |||
| de400f3473 | |||
| 8144a125ce | |||
| 3e34e41a5a | |||
| 2270887d43 | |||
| c6431f9a04 | |||
| 88c0d0120c | |||
| 44fefe5b9f | |||
| 878d368cea | |||
| f2bd026d0e | |||
| 518612492c | |||
| 81e43b87c2 | |||
| a02e17f15c | |||
| c76f86fdbd | |||
| 5b7c00ff52 | |||
| b9f0046ee7 | |||
| 3b79f7e4ae | |||
| 85d2df02b9 | |||
| 2f1e8ba612 | |||
| 84c690cb07 | |||
| 239bbad7ab | |||
| 4be8023408 | |||
| 83e8da57b8 | |||
| dcff6c996d | |||
| 0a66a6f1e5 | |||
| e82a5c5c54 | |||
| 92fdcafb66 | |||
| b9aae1aaae | |||
| 7d70afc937 | |||
| 12b63061c2 | |||
| 038fdeea83 | |||
| 0b6225bcc3 | |||
| f286ef8e17 | |||
| b120bcb60a | |||
| be34fc9134 | |||
| 8591d17d82 | |||
| f6190d6751 | |||
| f0fc77fded | |||
| f56cac6381 | |||
| 4f35054d29 | |||
| d29df6714a | |||
| 7460fb8349 | |||
| a7c430355a | |||
| 1df1517449 | |||
| f7c357ebad | |||
| 20c60aae68 | |||
| b14527b7af | |||
| f840080e5b | |||
| 2c6983a2f1 | |||
| acfb83ec5e | |||
| 3db931dc0e | |||
| 8309ddd486 | |||
| 21c868a646 | |||
| ffe9acfe4a | |||
| 24d904d194 | |||
| b280a37c4d | |||
| 906548d0ba | |||
| 1485a5bf3b | |||
| 9ec197f2e8 | |||
| d21466f595 | |||
| 4f3290309e | |||
| d6fe0f61a9 | |||
| 5a22f2cf0b | |||
| 42d11d9e7d | |||
| e49c184595 | |||
| 99d87c5ca2 | |||
| 4c0f48c548 | |||
| 4ce6d35e30 | |||
| 81bf0c66c6 | |||
| 34dc725d26 | |||
| a5db4ca092 | |||
| 61029fe20b | |||
| fee3f88cb5 | |||
| 932500e43d | |||
| 55d4cdd464 | |||
| fe3e47b1e8 | |||
| 9ca25bd48f | |||
| 91e0823ff0 | |||
| 142c6b11b3 | |||
| ef0b8367b5 | |||
| d1bfb4d2c0 | |||
| 3b5d6f003f | |||
| 26c457860b | |||
| d0515031c7 | |||
| 08f4a0a816 | |||
| 28f95f1fbe | |||
| c791de0e1e | |||
| 36b5426f6e | |||
| 1e72e9b1cd | |||
| 9739e55d0f | |||
| 1cddbc80cf | |||
| 3da9ee15d3 | |||
| 1e2fac054c | |||
| 914bfb2d9c | |||
| 40244994ad | |||
| 556ae07857 | |||
| 17fd71164c | |||
| 81d19156e9 | |||
| 7b82411e6f | |||
| fb268add7a | |||
| 7700973538 | |||
| 54e25a0251 | |||
| 79b3a1fe4e | |||
| faf013ec84 | |||
| 7152915318 | |||
| 886262e58a | |||
| 9c5d9ae376 | |||
| 3d2bc15e9a | |||
| 20c43c447d | |||
| 4caed7cc0d | |||
| 5b68f8ea6a | |||
| 8378bc9958 | |||
| 367cb48096 | |||
| 661b263e77 | |||
| 07c5e72cdb | |||
| 7752cdbfaf | |||
| 4545ecad58 | |||
| ac74237f01 | |||
| 5a36179c19 | |||
| 82d73f387d | |||
| e8c6314770 | |||
| 087c1b98dc | |||
| 68c5ad83fb | |||
| 5acc8c0134 | |||
| c897b6a82d | |||
| d008e90d50 | |||
| ea820b30bf | |||
| 03725dc015 | |||
| 0a6f9bc1eb | |||
| 1946922de3 | |||
| edf1f4233b | |||
| f4b55ea7a7 | |||
| 8dfd1f03e9 | |||
| acf26c5ab7 | |||
| d9800c8135 | |||
| 02bef7560f | |||
| 07dd0692b6 | |||
| 4f3317effe | |||
| 9afdbe3648 | |||
| fe0df01448 | |||
| 12e6907512 | |||
| 5aef492b4c | |||
| 5d7ed8ff7d | |||
| b1754fc5ff | |||
| 19bbf3e142 | |||
| e1755275a0 | |||
| 520037e721 | |||
| cbb0828ab8 | |||
| 8774d10bdf | |||
| df9f479d58 | |||
| 8bb52c9c2a | |||
| 947c423824 | |||
| c3d24fb26d | |||
| 112f9ae087 | |||
| 01b9ff54d9 | |||
| 64a1904136 | |||
| bce6864785 | |||
| ecd54b4cba | |||
| ca2b288a4b | |||
| 1016fbb8f9 | |||
| be3f81c7ec | |||
| 9f3c151c3c | |||
| 9735f3d8f2 | |||
| 34680c5ccf | |||
| 58934e5881 | |||
| ad3f98b8e7 | |||
| 7c33a33ef3 | |||
| 3dfcca68e6 | |||
| 73b74c94a1 | |||
| 18338d60d5 | |||
| e106070640 | |||
| 091a7ae359 | |||
| 70160aeab3 | |||
| 1aa08f594d | |||
| 14d8a931fe | |||
| 30ba85bc67 | |||
| caadcc3ed8 | |||
| 26f55472c6 | |||
| 79a58e275c | |||
| 900e584514 | |||
| bb639d9f29 | |||
| 15dcacc1fc | |||
| 6d53e3c2d7 | |||
| 8ed7346273 | |||
| 3c1220adca | |||
| 4ed0eb7012 | |||
| 2af5445309 | |||
| abb1916bda | |||
| 9424dca9e4 | |||
| db84bb9bd3 | |||
| c603f85488 | |||
| 2f1ee25f50 | |||
| 7bdf9005e5 | |||
| d9c1d79e30 | |||
| bd88b86919 | |||
| 8e29ae8c44 | |||
| d158607f8e | |||
| 939fbb3c38 | |||
| 3b9dfa9d29 | |||
| 0c76fb57f2 | |||
| 9694fa8d3a | |||
| 20761e053e | |||
| 29d885b40f | |||
| 087dc13965 | |||
| e7f559c582 | |||
| 52c5f6e152 | |||
| 26ca59859f | |||
| 23d6770ff9 | |||
| ac36a377b0 | |||
| 1642867136 | |||
| ce40392803 | |||
| 5f1af8a69d | |||
| c57ff2640e | |||
| d7fd396b7c | |||
| 45d145a823 | |||
| 221ef78faa | |||
| d86513cbba | |||
| 25b5904b84 | |||
| c2eb60df4a | |||
| feabd0430c | |||
| 838de23357 | |||
| d7b7040408 | |||
| 44e4bdc6f4 | |||
| 779060bc16 | |||
| 76239fa1ae | |||
| 5e53f767c4 | |||
| 974073a2e5 | |||
| d693431183 | |||
| bedf739d16 | |||
| 082755de1a | |||
| 6299e42aa9 | |||
| 129f41cee9 | |||
| 415bbc74aa | |||
| 13e41f2c68 | |||
| 1e117b780a | |||
| f8c5c24159 | |||
| f5a55c44d4 | |||
| 91a0e7bdaa | |||
| b07e309627 | |||
| 9ea45399ce | |||
| c19b1a143e | |||
| 02c24a860d | |||
| 9f652708ee | |||
| 05fa790584 | |||
| e0db822a9b | |||
| ec0fee6208 | |||
| a188554fe1 | |||
| 8d52c7daf3 | |||
| c49ebaaf1a | |||
| acc9645249 | |||
| 60f961dfe8 | |||
| 0c48b1d993 | |||
| d57b57bddc | |||
| 3837d2b94b | |||
| f81a188ef6 | |||
| 8e417e28d1 | |||
| 4ce6830a7b | |||
| 3a7c79e2c7 | |||
| cb2c2905c5 | |||
| 421160631a | |||
| 60aad1b717 | |||
| 72a17bdd76 | |||
| 9b9ce1c625 | |||
| d7cb6de820 | |||
| 3d5750f31c | |||
| fabf60bc4c | |||
| f5be8ba11f | |||
| 94d587e6fc | |||
| 2ec44f7620 | |||
| bbbf25201a | |||
| d6a3215fe2 | |||
| 75699a3825 | |||
| f3aeed77e5 | |||
| cfbaf7bf1c | |||
| bc6067d195 | |||
| 7203d93fb3 | |||
| ffd497673f | |||
| d00ff3c453 | |||
| 1d9e91e00f | |||
| 7f6ed35347 | |||
| 38027c8f52 | |||
| dd5804c10e | |||
| 84dcd25a36 | |||
| 4519450363 | |||
| 3c70eca758 | |||
| 68a2d6fc40 | |||
| a5923a5d51 | |||
| 769f0b1e24 | |||
| a1271d984f | |||
| db65ec4674 | |||
| a984c55cf9 | |||
| cdd0828c4a | |||
| 1984d0671b | |||
| 3e4efff73d | |||
| f4d1b7c603 | |||
| 200caab82b | |||
| f9b104f37b | |||
| c25b482301 | |||
| 31d7cc2cd4 | |||
| 19ecdc75a8 | |||
| 46724508f8 | |||
| 51b0194b8a | |||
| 9a27c4a2f0 | |||
| 32df742b85 | |||
| 8392765213 | |||
| 806b10b934 | |||
| 1fa0553c71 | |||
| 50a68a1791 | |||
| 0b55a0423e | |||
| 565d95a377 | |||
| f492f72154 | |||
| 4d84f0f6f0 | |||
| a0d0c8e4af | |||
| bef748abbd | |||
| c4373ef290 | |||
| 0b8c5a6872 | |||
| 829ecb2086 | |||
| d3564a4b09 | |||
| a244753f47 | |||
| 745782a77a | |||
| 246cbe1312 | |||
| 6c941122eb | |||
| 1a884cd8e1 | |||
| 18f008f7c7 | |||
| 6d42569ade | |||
| 5ed781a330 | |||
| 66fcd02aa2 | |||
| 1fc0e9a6aa | |||
| 91567ba916 | |||
| d80826b05d | |||
| 45bcf73185 | |||
| 78dc08bdc2 | |||
| f98f115ac2 | |||
| bf409936e7 | |||
| b4364723ef | |||
| bcc6359dec | |||
| 787a72a993 | |||
| d9eb962969 | |||
| f221153776 | |||
| 67596ef0cc | |||
| bf5bfe589f | |||
| af78c3925a | |||
| d144958669 | |||
| 5a64003f6f | |||
| 311718309c | |||
| 98479d7ffd | |||
| 90e505e58f | |||
| 410b8dd0fd | |||
| c2f25b6f62 | |||
| 03a2a74697 | |||
| 2807c11410 | |||
| 39d51ce845 | |||
| a216583d95 | |||
| 5c448b1b97 | |||
| 7f49c38e2d | |||
| 0e7fdcee30 | |||
| 418f8bed6a | |||
| 950fe73c4f | |||
| 0892f1e406 | |||
| 9af4e7b1da | |||
| 198a838d00 | |||
| aaa3fc3805 | |||
| 3c2efd9cf3 | |||
| 951b1e6a7a | |||
| 9c5fd6a776 | |||
| e438dccf19 | |||
| 43d2655ee4 | |||
| b2c92cdaaa | |||
| 42b1d92b2a | |||
| 1250312287 | |||
| 308371b434 | |||
| 88e6c11746 | |||
| 2ee4ae88f9 | |||
| e2b6fb0a6a | |||
| a19e3bc9d9 | |||
| 495c55e6a5 | |||
| a366143c5b | |||
| b8f5a7db33 | |||
| f486cfae86 | |||
| 0838732df3 | |||
| f422f09dff | |||
| aff196ae57 | |||
| 27c9f8be7a | |||
| 67b6c945e2 | |||
| c89aee37b9 | |||
| 03b7bebc96 | |||
| f89d0f68d0 | |||
| 72a288f73f | |||
| 0073aee1ed | |||
| 0f7a51f461 | |||
| 556552340a | |||
| 50bce0130a | |||
| 5c6dc63577 | |||
| 2077d27053 | |||
| 76b3d3c559 | |||
| 514b2d6f12 | |||
| 470553ff5d | |||
| 88e0aa1cb2 | |||
| 35f2552fc5 | |||
| e05886561d | |||
| 2451b9a75a | |||
| 06b71c99ee | |||
| ae8f7f11d5 | |||
| ed16ce9b73 | |||
| 27f895cf2c | |||
| c11a2ac396 | |||
| 2f9ab26372 | |||
| 0559f46bbb | |||
| 6e5f83c45b | |||
| e1b0582859 | |||
| 88d719689c | |||
| 200eb8dc0e | |||
| 082650bea3 | |||
| abf079135e | |||
| f00e8bc107 | |||
| ce05e67a0c | |||
| fecb1b0489 | |||
| 6a7e22386e | |||
| 85dfb4351c | |||
| addf15f61f | |||
| bbf3576f70 | |||
| da3f4bd452 | |||
| 5eb6f903f2 | |||
| 60394ddf83 | |||
| 293d261cf9 | |||
| f1cab828ee | |||
| 6a8d0fb955 | |||
| c8ca055935 | |||
| e0abb46616 | |||
| 170b89f468 | |||
| 674c6f7a7b | |||
| db35bcf2ce | |||
| 901d1314af | |||
| 9e9bfd0255 | |||
| 1080609c86 | |||
| 8315bcd0d8 | |||
| 7fb9301c03 | |||
| 63f3e5c3fc | |||
| a75412440f | |||
| 47de1d2e0e | |||
| 14fe8ecb58 | |||
| 0f01e7ef0f | |||
| 16f7c64a9f | |||
| 00aa9841b7 | |||
| 7802088e71 |
@@ -7,6 +7,11 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## NOTE
|
||||||
|
All GitHub issues are addressed on a best-effort basis at MinIO's sole discretion. There are no Service Level Agreements (SLA) or Objectives (SLO). Remember our [Code of Conduct](https://github.com/minio/minio/blob/master/code_of_conduct.md) when engaging with MinIO Engineers and the larger community.
|
||||||
|
|
||||||
|
For urgent issues (e.g. production down, etc.), subscribe to [SUBNET](https://min.io/pricing?jmp=github) for direct to engineering support.
|
||||||
|
|
||||||
<!--- Provide a general summary of the issue in the Title above -->
|
<!--- Provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
## Expected Behavior
|
## Expected Behavior
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## NOTE
|
||||||
|
If this case is urgent, please subscribe to [Subnet](https://min.io/pricing) so that our 24/7 support team may help you faster.
|
||||||
|
|
||||||
<!--- Provide a general summary of the issue in the Title above -->
|
<!--- Provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
## Expected Behavior
|
## Expected Behavior
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
|
|
||||||
|
|
||||||
# Number of days of inactivity before a closed issue or pull request is locked
|
|
||||||
daysUntilLock: 365
|
|
||||||
|
|
||||||
# Skip issues and pull requests created before a given timestamp. Timestamp must
|
|
||||||
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
|
|
||||||
skipCreatedBefore: false
|
|
||||||
|
|
||||||
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
|
|
||||||
exemptLabels: []
|
|
||||||
|
|
||||||
# Label to add before locking, such as `outdated`. Set to `false` to disable
|
|
||||||
lockLabel: true
|
|
||||||
|
|
||||||
# Comment to post before locking. Set to `false` to disable
|
|
||||||
lockComment: >-
|
|
||||||
|
|
||||||
This thread has been automatically locked since there has not been
|
|
||||||
any recent activity after it was closed. Please open a new issue for
|
|
||||||
related bugs.
|
|
||||||
|
|
||||||
# Assign `resolved` as the reason for locking. Set to `false` to disable
|
|
||||||
setLockReason: true
|
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
|
||||||
only: issues
|
|
||||||
|
|
||||||
# Optionally, specify configuration settings just for `issues` or `pulls`
|
|
||||||
# issues:
|
|
||||||
# exemptLabels:
|
|
||||||
# - help-wanted
|
|
||||||
# lockLabel: outdated
|
|
||||||
|
|
||||||
# pulls:
|
|
||||||
# daysUntilLock: 30
|
|
||||||
|
|
||||||
# Repository to extend settings from
|
|
||||||
# _extends: repo
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Config file for markdownlint-cli
|
||||||
|
MD033:
|
||||||
|
allowed_elements:
|
||||||
|
- details
|
||||||
|
- summary
|
||||||
+1
-1
@@ -14,7 +14,7 @@ onlyLabels: []
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- "security"
|
- "security"
|
||||||
- "pending discussion"
|
- "pending discussion"
|
||||||
- "do not close"
|
- "do-not-close"
|
||||||
|
|
||||||
# Set to true to ignore issues in a project (defaults to false)
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
exemptProjects: false
|
exemptProjects: false
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
name: Go
|
name: Crosscompile
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: MinIO crosscompile tests on ${{ matrix.go-version }} and ${{ matrix.os }}
|
name: Build Tests with Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.16.x, 1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
|
||||||
with:
|
|
||||||
node-version: '12'
|
|
||||||
- uses: actions/setup-go@v2
|
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- name: Build on ${{ matrix.os }}
|
- name: Build on ${{ matrix.os }}
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: FIPS Build Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go BoringCrypto ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.11b7, 1.18.3b7]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Setup dockerfile for build test
|
||||||
|
run: |
|
||||||
|
echo "FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:${{ matrix.go-version }}" > Dockerfile.fips.test
|
||||||
|
echo "COPY . /minio" >> Dockerfile.fips.test
|
||||||
|
echo "WORKDIR /minio" >> Dockerfile.fips.test
|
||||||
|
echo "RUN make" >> Dockerfile.fips.test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.fips.test
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: minio/fips-test:latest
|
||||||
|
|
||||||
|
# This should fail if grep returns non-zero exit
|
||||||
|
- name: Test binary
|
||||||
|
run: |
|
||||||
|
docker run --rm minio/fips-test:latest ./minio --version
|
||||||
|
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio' | grep -q FIPS
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Healing Functional Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
|
- name: Build on ${{ matrix.os }}
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GO111MODULE: on
|
||||||
|
MINIO_KMS_SECRET_KEY: "my-minio-key:oyArl7zlPECEduNbB1KXgdzDn2Bdpvvw0l8VO51HQnY="
|
||||||
|
MINIO_KMS_AUTO_ENCRYPTION: on
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make verify-healing
|
||||||
|
make verify-healing-inconsistent-versions
|
||||||
|
make verify-healing-with-root-disks
|
||||||
@@ -1,23 +1,51 @@
|
|||||||
name: Go
|
name: Linters and Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: MinIO tests on ${{ matrix.go-version }} and ${{ matrix.os }}
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.16.x, 1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest, windows-latest]
|
os: [ubuntu-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
%LocalAppData%\go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
- name: Build on ${{ matrix.os }}
|
- name: Build on ${{ matrix.os }}
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
env:
|
env:
|
||||||
@@ -39,4 +67,5 @@ jobs:
|
|||||||
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy
|
curl -L -o nancy https://github.com/sonatype-nexus-community/nancy/releases/download/${nancy_version}/nancy-${nancy_version}-linux-amd64 && chmod +x nancy
|
||||||
go list -deps -json ./... | jq -s 'unique_by(.Module.Path)|.[]|select(has("Module"))|.Module' | ./nancy sleuth
|
go list -deps -json ./... | jq -s 'unique_by(.Module.Path)|.[]|select(has("Module"))|.Module' | ./nancy sleuth
|
||||||
make
|
make
|
||||||
|
make test
|
||||||
make test-race
|
make test-race
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
name: Go
|
name: Functional Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: MinIO Setup on ${{ matrix.go-version }} and ${{ matrix.os }}
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }} - healing
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.16.x, 1.17.x]
|
go-version: [1.17.x, 1.18.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
- name: Build on ${{ matrix.os }}
|
- name: Build on ${{ matrix.os }}
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
env:
|
env:
|
||||||
@@ -32,4 +50,3 @@ jobs:
|
|||||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
make verify
|
make verify
|
||||||
make verify-healing
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
name: IAM integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
iam-matrix-test:
|
||||||
|
name: "[Go=${{ matrix.go-version }}|ldap=${{ matrix.ldap }}|etcd=${{ matrix.etcd }}|openid=${{ matrix.openid }}]"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
openldap:
|
||||||
|
image: quay.io/minio/openldap
|
||||||
|
ports:
|
||||||
|
- "389:389"
|
||||||
|
- "636:636"
|
||||||
|
env:
|
||||||
|
LDAP_ORGANIZATION: "MinIO Inc"
|
||||||
|
LDAP_DOMAIN: "min.io"
|
||||||
|
LDAP_ADMIN_PASSWORD: "admin"
|
||||||
|
etcd:
|
||||||
|
image: "quay.io/coreos/etcd:v3.5.1"
|
||||||
|
env:
|
||||||
|
ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379"
|
||||||
|
ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379"
|
||||||
|
ports:
|
||||||
|
- "2379:2379"
|
||||||
|
options: >-
|
||||||
|
--health-cmd "etcdctl endpoint health"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
openid:
|
||||||
|
image: quay.io/minio/dex
|
||||||
|
ports:
|
||||||
|
- "5556:5556"
|
||||||
|
env:
|
||||||
|
DEX_LDAP_SERVER: "openldap:389"
|
||||||
|
openid2:
|
||||||
|
image: quay.io/minio/dex
|
||||||
|
ports:
|
||||||
|
- "5557:5557"
|
||||||
|
env:
|
||||||
|
DEX_LDAP_SERVER: "openldap:389"
|
||||||
|
DEX_ISSUER: "http://127.0.0.1:5557/dex"
|
||||||
|
DEX_WEB_HTTP: "0.0.0.0:5557"
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
# When ldap, etcd or openid vars are empty below, those external servers
|
||||||
|
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||||
|
# the tests.
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
ldap: ["", "localhost:389"]
|
||||||
|
etcd: ["", "http://localhost:2379"]
|
||||||
|
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||||
|
exclude:
|
||||||
|
# exclude combos where all are empty.
|
||||||
|
- ldap: ""
|
||||||
|
etcd: ""
|
||||||
|
openid: ""
|
||||||
|
# exclude combos where both ldap and openid IDPs are specified.
|
||||||
|
- ldap: "localhost:389"
|
||||||
|
openid: "http://127.0.0.1:5556/dex"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
|
- name: Test LDAP/OpenID/Etcd combo
|
||||||
|
env:
|
||||||
|
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||||
|
ETCD_SERVER: ${{ matrix.etcd }}
|
||||||
|
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-iam
|
||||||
|
- name: Test with multiple OpenID providers
|
||||||
|
if: matrix.openid == 'http://127.0.0.1:5556/dex'
|
||||||
|
env:
|
||||||
|
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||||
|
ETCD_SERVER: ${{ matrix.etcd }}
|
||||||
|
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||||
|
OPENID_TEST_SERVER_2: "http://127.0.0.1:5557/dex"
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-iam
|
||||||
|
- name: Test with Access Management Plugin enabled
|
||||||
|
env:
|
||||||
|
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||||
|
ETCD_SERVER: ${{ matrix.etcd }}
|
||||||
|
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||||
|
POLICY_PLUGIN_ENDPOINT: "http://127.0.0.1:8080"
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
go run docs/iam/access-manager-plugin.go &
|
||||||
|
make test-iam
|
||||||
|
- name: Test LDAP for automatic site replication
|
||||||
|
if: matrix.ldap == 'localhost:389'
|
||||||
|
run: |
|
||||||
|
make test-site-replication-ldap
|
||||||
|
- name: Test OIDC for automatic site replication
|
||||||
|
if: matrix.openid == 'http://127.0.0.1:5556/dex'
|
||||||
|
run: |
|
||||||
|
make test-site-replication-oidc
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: 'Lock Threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lock
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v3
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-inactive-days: '365'
|
||||||
|
exclude-any-issue-labels: 'do-not-close'
|
||||||
|
issue-lock-reason: 'resolved'
|
||||||
|
log-output: true
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Markdown Linter
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint all docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Lint all docs
|
||||||
|
run: |
|
||||||
|
npm install -g markdownlint-cli
|
||||||
|
markdownlint --fix '**/*.md' \
|
||||||
|
--config /home/runner/work/minio/minio/.github/markdown-lint-cfg.yaml \
|
||||||
|
--disable MD013 MD040
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: MinIO advanced tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
replication-test:
|
||||||
|
name: Advanced Tests with Go ${{ matrix.go-version }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.go-version }}-go-
|
||||||
|
- name: Test Decom
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-decom
|
||||||
|
|
||||||
|
- name: Test Replication
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-replication
|
||||||
|
|
||||||
|
- name: Test MinIO IDP for automatic site replication
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-site-replication-minio
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Upgrade old version tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.17.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Start upgrade tests
|
||||||
|
run: |
|
||||||
|
make test-upgrade
|
||||||
+18
-2
@@ -9,8 +9,7 @@ site/
|
|||||||
/.idea/
|
/.idea/
|
||||||
/Minio.iml
|
/Minio.iml
|
||||||
**/access.log
|
**/access.log
|
||||||
vendor/**/*.js
|
vendor/
|
||||||
vendor/**/*.json
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.syso
|
*.syso
|
||||||
coverage.txt
|
coverage.txt
|
||||||
@@ -22,3 +21,20 @@ stage/
|
|||||||
.sia_temp/
|
.sia_temp/
|
||||||
config.json
|
config.json
|
||||||
node_modules/
|
node_modules/
|
||||||
|
mc.*
|
||||||
|
s3-check-md5*
|
||||||
|
xl-meta*
|
||||||
|
healing-*
|
||||||
|
inspect*
|
||||||
|
200M*
|
||||||
|
hash-set
|
||||||
|
minio.RELEASE*
|
||||||
|
mc
|
||||||
|
nancy
|
||||||
|
inspects/*
|
||||||
|
docs/debugging/s3-verify/s3-verify
|
||||||
|
docs/debugging/xl-meta/xl-meta
|
||||||
|
docs/debugging/s3-check-md5/s3-check-md5
|
||||||
|
docs/debugging/hash-set/hash-set
|
||||||
|
docs/debugging/healing-bin/healing-bin
|
||||||
|
docs/debugging/inspect/inspect
|
||||||
|
|||||||
+19
-1
@@ -23,12 +23,30 @@ linters:
|
|||||||
- structcheck
|
- structcheck
|
||||||
- unconvert
|
- unconvert
|
||||||
- varcheck
|
- varcheck
|
||||||
|
- gocritic
|
||||||
|
- gofumpt
|
||||||
|
- tenv
|
||||||
|
- durationcheck
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
gofumpt:
|
||||||
|
lang-version: "1.17"
|
||||||
|
|
||||||
|
# Choose whether or not to use the extra rules that are disabled
|
||||||
|
# by default
|
||||||
|
extra-rules: false
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
exclude:
|
exclude:
|
||||||
- should have a package comment
|
- should have a package comment
|
||||||
- error strings should not be capitalized or end with punctuation or a newline
|
- error strings should not be capitalized or end with punctuation or a newline
|
||||||
|
# todo fix these when we get enough time.
|
||||||
|
- "singleCaseSwitch: should rewrite switch statement to if statement"
|
||||||
|
- "unlambda: replace"
|
||||||
|
- "captLocal:"
|
||||||
|
- "ifElseChain:"
|
||||||
|
- "elseif:"
|
||||||
|
|
||||||
service:
|
service:
|
||||||
golangci-lint-version: 1.20.0 # use the fixed version to not introduce new linters unexpectedly
|
golangci-lint-version: 1.43.0 # use the fixed version to not introduce new linters unexpectedly
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# AGPLv3 Compliance
|
||||||
|
|
||||||
|
We have designed MinIO as an Open Source software for the Open Source software community. This requires applications to consider whether their usage of MinIO is in compliance with the GNU AGPLv3 [license](https://github.com/minio/minio/blob/master/LICENSE).
|
||||||
|
|
||||||
|
MinIO cannot make the determination as to whether your application's usage of MinIO is in compliance with the AGPLv3 license requirements. You should instead rely on your own legal counsel or licensing specialists to audit and ensure your application is in compliance with the licenses of MinIO and all other open-source projects with which your application integrates or interacts. We understand that AGPLv3 licensing is complex and nuanced. It is for that reason we strongly encourage using experts in licensing to make any such determinations around compliance instead of relying on apocryphal or anecdotal advice.
|
||||||
|
|
||||||
|
[MinIO Commercial Licensing](https://min.io/pricing) is the best option for applications that trigger AGPLv3 obligations (e.g. open sourcing your application). Applications using MinIO - or any other OSS-licensed code - without validating their usage do so at their own risk.
|
||||||
+19
-6
@@ -7,15 +7,17 @@
|
|||||||
Start by forking the MinIO GitHub repository, make changes in a branch and then send a pull request. We encourage pull requests to discuss code changes. Here are the steps in details:
|
Start by forking the MinIO GitHub repository, make changes in a branch and then send a pull request. We encourage pull requests to discuss code changes. Here are the steps in details:
|
||||||
|
|
||||||
### Setup your MinIO GitHub Repository
|
### Setup your MinIO GitHub Repository
|
||||||
|
|
||||||
Fork [MinIO upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL of your MinIO fork (you will need it for the `git clone` command below).
|
Fork [MinIO upstream](https://github.com/minio/minio/fork) source repository to your own personal repository. Copy the URL of your MinIO fork (you will need it for the `git clone` command below).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ git clone https://github.com/minio/minio
|
git clone https://github.com/minio/minio
|
||||||
$ go install -v
|
go install -v
|
||||||
$ ls /go/bin/minio
|
ls /go/bin/minio
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set up git remote as ``upstream``
|
### Set up git remote as ``upstream``
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ cd minio
|
$ cd minio
|
||||||
$ git remote add upstream https://github.com/minio/minio
|
$ git remote add upstream https://github.com/minio/minio
|
||||||
@@ -25,13 +27,15 @@ $ git merge upstream/master
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Create your feature branch
|
### Create your feature branch
|
||||||
|
|
||||||
Before making code changes, make sure you create a separate branch for these changes
|
Before making code changes, make sure you create a separate branch for these changes
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git checkout -b my-new-feature
|
git checkout -b my-new-feature
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test MinIO server changes
|
### Test MinIO server changes
|
||||||
|
|
||||||
After your code changes, make sure
|
After your code changes, make sure
|
||||||
|
|
||||||
- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](https://slack.min.io) channel.
|
- To add test cases for the new code. If you have questions about how to do it, please ask on our [Slack](https://slack.min.io) channel.
|
||||||
@@ -40,29 +44,38 @@ After your code changes, make sure
|
|||||||
- To run `make test` and `make build` completes.
|
- To run `make test` and `make build` completes.
|
||||||
|
|
||||||
### Commit changes
|
### Commit changes
|
||||||
|
|
||||||
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
After verification, commit your changes. This is a [great post](https://chris.beams.io/posts/git-commit/) on how to write useful commit messages
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git commit -am 'Add some feature'
|
git commit -am 'Add some feature'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Push to the branch
|
### Push to the branch
|
||||||
|
|
||||||
Push your locally committed changes to the remote origin (your fork)
|
Push your locally committed changes to the remote origin (your fork)
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git push origin my-new-feature
|
git push origin my-new-feature
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a Pull Request
|
### Create a Pull Request
|
||||||
|
|
||||||
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
Pull requests can be created via GitHub. Refer to [this document](https://help.github.com/articles/creating-a-pull-request/) for detailed steps on how to create a pull request. After a Pull Request gets peer reviewed and approved, it will be merged.
|
||||||
|
|
||||||
## FAQs
|
## FAQs
|
||||||
|
|
||||||
### How does ``MinIO`` manage dependencies?
|
### How does ``MinIO`` manage dependencies?
|
||||||
|
|
||||||
``MinIO`` uses `go mod` to manage its dependencies.
|
``MinIO`` uses `go mod` to manage its dependencies.
|
||||||
|
|
||||||
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
|
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
|
||||||
|
|
||||||
To remove a dependency
|
To remove a dependency
|
||||||
|
|
||||||
- Edit your code and remove the import reference.
|
- Edit your code and remove the import reference.
|
||||||
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
|
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
|
||||||
|
|
||||||
### What are the coding guidelines for MinIO?
|
### What are the coding guidelines for MinIO?
|
||||||
|
|
||||||
``MinIO`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
``MinIO`` is fully conformant with Golang style. Refer: [Effective Go](https://github.com/golang/go/wiki/CodeReviewComments) article from Golang project. If you observe offending code, please feel free to send a pull request or ping us on [Slack](https://slack.min.io).
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
FROM minio/minio:latest
|
FROM minio/minio:latest
|
||||||
|
|
||||||
|
ENV PATH=/opt/bin:$PATH
|
||||||
|
|
||||||
|
COPY ./minio /opt/bin/minio
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|||||||
+4
-9
@@ -1,14 +1,9 @@
|
|||||||
FROM minio/minio:edge
|
FROM minio/minio:latest
|
||||||
|
|
||||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
ENV PATH=/opt/bin:$PATH
|
||||||
|
|
||||||
COPY minio /usr/bin/
|
COPY ./minio /opt/bin/minio
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
RUN chmod +x /usr/bin/minio && \
|
|
||||||
chmod +x /usr/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 9000
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||||
|
|
||||||
|
ARG RELEASE
|
||||||
|
|
||||||
|
LABEL name="MinIO" \
|
||||||
|
vendor="MinIO Inc <dev@min.io>" \
|
||||||
|
maintainer="MinIO Inc <dev@min.io>" \
|
||||||
|
version="${RELEASE}" \
|
||||||
|
release="${RELEASE}" \
|
||||||
|
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
|
||||||
|
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
||||||
|
|
||||||
|
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||||
|
MINIO_SECRET_KEY_FILE=secret_key \
|
||||||
|
MINIO_ROOT_USER_FILE=access_key \
|
||||||
|
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||||
|
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||||
|
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
||||||
|
MINIO_CONFIG_ENV_FILE=config.env \
|
||||||
|
PATH=/opt/bin:$PATH
|
||||||
|
|
||||||
|
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
|
||||||
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||||
|
COPY CREDITS /licenses/CREDITS
|
||||||
|
COPY LICENSE /licenses/LICENSE
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
microdnf clean all && \
|
||||||
|
microdnf update --nodocs && \
|
||||||
|
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
|
||||||
|
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
||||||
|
microdnf install minisign --nodocs && \
|
||||||
|
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
||||||
|
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE} -o /opt/bin/minio && \
|
||||||
|
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
|
||||||
|
curl -s -q https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.${RELEASE}.minisig -o /opt/bin/minio.minisig && \
|
||||||
|
microdnf clean all && \
|
||||||
|
chmod +x /opt/bin/minio && \
|
||||||
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
|
chmod +x /usr/bin/verify-minio.sh && \
|
||||||
|
/usr/bin/verify-minio.sh && \
|
||||||
|
microdnf clean all
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
CMD ["minio"]
|
||||||
+12
-8
@@ -1,4 +1,4 @@
|
|||||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
|
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
|||||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||||
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
||||||
MINIO_CONFIG_ENV_FILE=config.env
|
MINIO_CONFIG_ENV_FILE=config.env \
|
||||||
|
PATH=/opt/bin:$PATH
|
||||||
|
|
||||||
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
|
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||||
@@ -26,18 +27,21 @@ COPY CREDITS /licenses/CREDITS
|
|||||||
COPY LICENSE /licenses/LICENSE
|
COPY LICENSE /licenses/LICENSE
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
|
microdnf clean all && \
|
||||||
microdnf update --nodocs && \
|
microdnf update --nodocs && \
|
||||||
microdnf install curl ca-certificates shadow-utils util-linux iproute iputils --nodocs && \
|
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
|
||||||
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
||||||
microdnf install minisign --nodocs && \
|
microdnf install minisign --nodocs && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /usr/bin/minio && \
|
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.sha256sum -o /usr/bin/minio.sha256sum && \
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE} -o /opt/bin/minio && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /usr/bin/minio.minisig && \
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.sha256sum -o /opt/bin/minio.sha256sum && \
|
||||||
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.minisig -o /opt/bin/minio.minisig && \
|
||||||
microdnf clean all && \
|
microdnf clean all && \
|
||||||
chmod +x /usr/bin/minio && \
|
chmod +x /opt/bin/minio && \
|
||||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
chmod +x /usr/bin/verify-minio.sh && \
|
chmod +x /usr/bin/verify-minio.sh && \
|
||||||
/usr/bin/verify-minio.sh
|
/usr/bin/verify-minio.sh && \
|
||||||
|
microdnf clean all
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
|
|||||||
+12
-8
@@ -1,4 +1,4 @@
|
|||||||
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4
|
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
|||||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||||
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
||||||
MINIO_CONFIG_ENV_FILE=config.env
|
MINIO_CONFIG_ENV_FILE=config.env \
|
||||||
|
PATH=/opt/bin:$PATH
|
||||||
|
|
||||||
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
|
COPY dockerscripts/verify-minio.sh /usr/bin/verify-minio.sh
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||||
@@ -26,18 +27,21 @@ COPY CREDITS /licenses/CREDITS
|
|||||||
COPY LICENSE /licenses/LICENSE
|
COPY LICENSE /licenses/LICENSE
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
|
microdnf clean all && \
|
||||||
microdnf update --nodocs && \
|
microdnf update --nodocs && \
|
||||||
microdnf install curl ca-certificates shadow-utils util-linux iproute iputils --nodocs && \
|
microdnf install curl ca-certificates shadow-utils util-linux --nodocs && \
|
||||||
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm && \
|
||||||
microdnf install minisign --nodocs && \
|
microdnf install minisign --nodocs && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /usr/bin/minio && \
|
mkdir -p /opt/bin && chmod -R 777 /opt/bin && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /usr/bin/minio.sha256sum && \
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /opt/bin/minio && \
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /usr/bin/minio.minisig && \
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /opt/bin/minio.sha256sum && \
|
||||||
|
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /opt/bin/minio.minisig && \
|
||||||
microdnf clean all && \
|
microdnf clean all && \
|
||||||
chmod +x /usr/bin/minio && \
|
chmod +x /opt/bin/minio && \
|
||||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||||
chmod +x /usr/bin/verify-minio.sh && \
|
chmod +x /usr/bin/verify-minio.sh && \
|
||||||
/usr/bin/verify-minio.sh
|
/usr/bin/verify-minio.sh && \
|
||||||
|
microdnf clean all
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY minio /minio
|
||||||
|
|
||||||
|
CMD ["/minio"]
|
||||||
@@ -10,78 +10,130 @@ TAG ?= "minio/minio:$(VERSION)"
|
|||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
checks:
|
checks: ## check dependencies
|
||||||
@echo "Checking dependencies"
|
@echo "Checking dependencies"
|
||||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||||
|
|
||||||
getdeps:
|
help: ## print this help
|
||||||
@mkdir -p ${GOPATH}/bin
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.40.1
|
|
||||||
@which msgp 1>/dev/null || (echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.3)
|
|
||||||
@which stringer 1>/dev/null || (echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer)
|
|
||||||
|
|
||||||
crosscompile:
|
getdeps: ## fetch necessary dependencies
|
||||||
|
@mkdir -p ${GOPATH}/bin
|
||||||
|
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin v1.45.2
|
||||||
|
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.7-0.20211026165309-e818a1881b0e
|
||||||
|
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
|
||||||
|
|
||||||
|
crosscompile: ## cross compile minio
|
||||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||||
|
|
||||||
verifiers: getdeps lint check-gen
|
verifiers: getdeps lint check-gen
|
||||||
|
|
||||||
check-gen:
|
check-gen: ## check for updated autogenerated files
|
||||||
@go generate ./... >/dev/null
|
@go generate ./... >/dev/null
|
||||||
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
@(! git diff --name-only | grep '_gen.go$$') || (echo "Non-committed changes in auto-generated code is detected, please commit them to proceed." && false)
|
||||||
|
|
||||||
lint:
|
lint: ## runs golangci-lint suite of linters
|
||||||
@echo "Running $@ check"
|
@echo "Running $@ check"
|
||||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint cache clean
|
@${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
||||||
@GO111MODULE=on ${GOPATH}/bin/golangci-lint run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
|
||||||
|
|
||||||
# Builds minio, runs the verifiers then runs the tests.
|
|
||||||
check: test
|
check: test
|
||||||
test: verifiers build
|
test: verifiers build ## builds minio, runs linters, tests
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@GOGC=25 GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
|
@CGO_ENABLED=0 go test -tags kqueue ./...
|
||||||
|
|
||||||
test-race: verifiers build
|
test-decom: install
|
||||||
|
@echo "Running minio decom tests"
|
||||||
|
@env bash $(PWD)/docs/distributed/decom.sh
|
||||||
|
|
||||||
|
test-upgrade: build
|
||||||
|
@echo "Running minio upgrade tests"
|
||||||
|
@(env bash $(PWD)/buildscripts/minio-upgrade.sh)
|
||||||
|
|
||||||
|
test-race: verifiers build ## builds minio, runs linters, tests (race)
|
||||||
@echo "Running unit tests under -race"
|
@echo "Running unit tests under -race"
|
||||||
@(env bash $(PWD)/buildscripts/race.sh)
|
@(env bash $(PWD)/buildscripts/race.sh)
|
||||||
|
|
||||||
# Verify minio binary
|
test-iam: build ## verify IAM (external IDP, etcd backends)
|
||||||
verify:
|
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||||
|
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
||||||
|
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||||
|
@GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||||
|
|
||||||
|
test-replication: install ## verify multi site replication
|
||||||
|
@echo "Running tests for replicating three sites"
|
||||||
|
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||||
|
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||||
|
|
||||||
|
test-site-replication-ldap: install ## verify automatic site replication
|
||||||
|
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
||||||
|
@(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh)
|
||||||
|
|
||||||
|
test-site-replication-oidc: install ## verify automatic site replication
|
||||||
|
@echo "Running tests for automatic site replication of IAM (with OIDC)"
|
||||||
|
@(env bash $(PWD)/docs/site-replication/run-multi-site-oidc.sh)
|
||||||
|
|
||||||
|
test-site-replication-minio: install ## verify automatic site replication
|
||||||
|
@echo "Running tests for automatic site replication of IAM (with MinIO IDP)"
|
||||||
|
@(env bash $(PWD)/docs/site-replication/run-multi-site-minio-idp.sh)
|
||||||
|
|
||||||
|
verify: ## verify minio various setups
|
||||||
@echo "Verifying build with race"
|
@echo "Verifying build with race"
|
||||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
|
|
||||||
# Verify healing of disks with minio binary
|
verify-healing: ## verify healing and replacing disks with minio binary
|
||||||
verify-healing:
|
|
||||||
@echo "Verify healing build with race"
|
@echo "Verify healing build with race"
|
||||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||||
|
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
|
||||||
|
|
||||||
# Builds minio locally.
|
verify-healing-with-root-disks:
|
||||||
build: checks
|
@echo "Verify healing with root disks"
|
||||||
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
@(env bash $(PWD)/buildscripts/verify-healing-with-root-disks.sh)
|
||||||
|
|
||||||
|
verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
|
||||||
|
@echo "Verify resolving inconsistent versions build with race"
|
||||||
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
|
||||||
|
|
||||||
|
build: checks ## builds minio to $(PWD)
|
||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
|
||||||
hotfix-vars:
|
hotfix-vars:
|
||||||
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
|
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
|
||||||
sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#')))
|
sed 's#RELEASE\.\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)T\([0-9]\+\)-\([0-9]\+\)-\([0-9]\+\)Z#\1-\2-\3T\4:\5:\6Z#')))
|
||||||
$(eval TAG := "minio/minio:$(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD)")
|
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
||||||
hotfix: hotfix-vars install
|
$(eval TAG := "minio/minio:$(VERSION)")
|
||||||
|
|
||||||
docker-hotfix: hotfix checks
|
hotfix: hotfix-vars install ## builds minio binary with hotfix tags
|
||||||
|
@mv -f ./minio ./minio.$(VERSION)
|
||||||
|
@minisign -qQSm ./minio.$(VERSION) -s "${CRED_DIR}/minisign.key" < "${CRED_DIR}/minisign-passphrase"
|
||||||
|
@sha256sum < ./minio.$(VERSION) | sed 's, -,minio.$(VERSION),g' > minio.$(VERSION).sha256sum
|
||||||
|
|
||||||
|
hotfix-push: hotfix
|
||||||
|
@scp -q -r minio.$(VERSION)* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive/
|
||||||
|
@scp -q -r minio.$(VERSION)* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive/
|
||||||
|
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.$(VERSION)"
|
||||||
|
|
||||||
|
docker-hotfix-push: docker-hotfix
|
||||||
|
@docker push -q $(TAG) && echo "Published new container $(TAG)"
|
||||||
|
|
||||||
|
docker-hotfix: hotfix-push checks ## builds minio docker container with hotfix tags
|
||||||
@echo "Building minio docker image '$(TAG)'"
|
@echo "Building minio docker image '$(TAG)'"
|
||||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
@docker build -q --no-cache -t $(TAG) --build-arg RELEASE=$(VERSION) . -f Dockerfile.hotfix
|
||||||
|
|
||||||
docker: build checks
|
docker: build checks ## builds minio docker container
|
||||||
@echo "Building minio docker image '$(TAG)'"
|
@echo "Building minio docker image '$(TAG)'"
|
||||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
@docker build -q --no-cache -t $(TAG) . -f Dockerfile
|
||||||
|
|
||||||
# Builds minio and installs it to $GOPATH/bin.
|
install: build ## builds minio and installs it to $GOPATH/bin.
|
||||||
install: build
|
|
||||||
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
|
@echo "Installing minio binary to '$(GOPATH)/bin/minio'"
|
||||||
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio
|
@mkdir -p $(GOPATH)/bin && cp -f $(PWD)/minio $(GOPATH)/bin/minio
|
||||||
@echo "Installation successful. To learn more, try \"minio --help\"."
|
@echo "Installation successful. To learn more, try \"minio --help\"."
|
||||||
|
|
||||||
clean:
|
clean: ## cleanup all generated assets
|
||||||
@echo "Cleaning up all the generated files"
|
@echo "Cleaning up all the generated files"
|
||||||
@find . -name '*.test' | xargs rm -fv
|
@find . -name '*.test' | xargs rm -fv
|
||||||
@find . -name '*~' | xargs rm -fv
|
@find . -name '*~' | xargs rm -fv
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# MinIO FIPS Builds
|
||||||
|
|
||||||
|
MinIO creates FIPS builds using a patched version of the Go compiler (that uses BoringCrypto, from BoringSSL, which is [FIPS 140-2 validated](https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2964.pdf)) published by the Golang Team [here](https://github.com/golang/go/tree/dev.boringcrypto/misc/boring).
|
||||||
|
|
||||||
|
MinIO FIPS executables are available at http://dl.min.io - they are only published for `linux-amd64` architecture as binary files with the suffix `.fips`. We also publish corresponding container images to our official image repositories.
|
||||||
|
|
||||||
|
We are not making any statements or representations about the suitability of this code or build in relation to the FIPS 140-2 standard. Interested users will have to evaluate for themselves whether this is useful for their own purposes.
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
# MinIO Quickstart Guide
|
# MinIO Quickstart Guide
|
||||||
|
|
||||||
[](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/) [](https://github.com/minio/minio/blob/master/LICENSE)
|
[](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/) [](https://github.com/minio/minio/blob/master/LICENSE)
|
||||||
|
|
||||||
[](https://min.io)
|
[](https://min.io)
|
||||||
|
|
||||||
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
|
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
|
||||||
|
|
||||||
This README provides quickstart instructions on running MinIO on baremetal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
|
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
|
||||||
|
|
||||||
# Container Installation
|
## Container Installation
|
||||||
|
|
||||||
Use the following commands to run a standalone MinIO server as a container.
|
Use the following commands to run a standalone MinIO server as a container.
|
||||||
|
|
||||||
@@ -16,34 +17,32 @@ require distributed deploying MinIO with Erasure Coding. For extended developmen
|
|||||||
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
|
with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html)
|
||||||
for more complete documentation.
|
for more complete documentation.
|
||||||
|
|
||||||
## Stable
|
### Stable
|
||||||
|
|
||||||
Run the following command to run the latest stable image of MinIO as a container using an ephemeral data volume:
|
Run the following command to run the latest stable image of MinIO as a container using an ephemeral data volume:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
podman run \
|
podman run -p 9000:9000 -p 9001:9001 \
|
||||||
-p 9000:9000 \
|
quay.io/minio/minio server /data --console-address ":9001"
|
||||||
-p 9001:9001 \
|
|
||||||
minio/minio server /data --console-address ":9001"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded
|
||||||
object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the
|
object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the
|
||||||
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See
|
||||||
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
|
[Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers,
|
||||||
see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
> NOTE: To deploy MinIO on with persistent storage, you must map local persistent directories from the host OS to the container using the `podman -v` option. For example, `-v /mnt/data:/data` maps the host OS drive at `/mnt/data` to `/data` on the container.
|
> NOTE: To deploy MinIO on with persistent storage, you must map local persistent directories from the host OS to the container using the `podman -v` option. For example, `-v /mnt/data:/data` maps the host OS drive at `/mnt/data` to `/data` on the container.
|
||||||
|
|
||||||
# macOS
|
## macOS
|
||||||
|
|
||||||
Use the following commands to run a standalone MinIO server on macOS.
|
Use the following commands to run a standalone MinIO server on macOS.
|
||||||
|
|
||||||
Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
||||||
|
|
||||||
## Homebrew (recommended)
|
### Homebrew (recommended)
|
||||||
|
|
||||||
Run the following command to install the latest stable MinIO package using [Homebrew](https://brew.sh/). Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
Run the following command to install the latest stable MinIO package using [Homebrew](https://brew.sh/). Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
||||||
|
|
||||||
@@ -59,11 +58,11 @@ brew uninstall minio
|
|||||||
brew install minio/stable/minio
|
brew install minio/stable/minio
|
||||||
```
|
```
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
## Binary Download
|
### Binary Download
|
||||||
|
|
||||||
Use the following command to download and run a standalone MinIO server on macOS. Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
Use the following command to download and run a standalone MinIO server on macOS. Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
||||||
|
|
||||||
@@ -73,11 +72,11 @@ chmod +x minio
|
|||||||
./minio server /data
|
./minio server /data
|
||||||
```
|
```
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
# GNU/Linux
|
## GNU/Linux
|
||||||
|
|
||||||
Use the following command to run a standalone MinIO server on Linux hosts running 64-bit Intel/AMD architectures. Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
Use the following command to run a standalone MinIO server on Linux hosts running 64-bit Intel/AMD architectures. Replace ``/data`` with the path to the drive or directory in which you want MinIO to store data.
|
||||||
|
|
||||||
@@ -93,18 +92,18 @@ The following table lists supported architectures. Replace the `wget` URL with t
|
|||||||
|
|
||||||
| Architecture | URL |
|
| Architecture | URL |
|
||||||
| -------- | ------ |
|
| -------- | ------ |
|
||||||
| 64-bit Intel/AMD | https://dl.min.io/server/minio/release/linux-amd64/minio |
|
| 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> |
|
||||||
| 64-bit ARM | https://dl.min.io/server/minio/release/linux-arm64/minio |
|
| 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> |
|
||||||
| 64-bit PowerPC LE (ppc64le) | https://dl.min.io/server/minio/release/linux-ppc64le/minio |
|
| 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> |
|
||||||
| IBM Z-Series (S390X) | https://dl.min.io/server/minio/release/linux-s390x/minio |
|
| IBM Z-Series (S390X) | <https://dl.min.io/server/minio/release/linux-s390x/minio> |
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
||||||
|
|
||||||
# Microsoft Windows
|
## Microsoft Windows
|
||||||
|
|
||||||
To run MinIO on 64-bit Windows hosts, download the MinIO executable from the following URL:
|
To run MinIO on 64-bit Windows hosts, download the MinIO executable from the following URL:
|
||||||
|
|
||||||
@@ -118,31 +117,31 @@ Use the following command to run a standalone MinIO server on the Windows host.
|
|||||||
minio.exe server D:\
|
minio.exe server D:\
|
||||||
```
|
```
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
||||||
|
|
||||||
# Install from Source
|
## Install from Source
|
||||||
|
|
||||||
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.16](https://golang.org/dl/#stable)
|
Use the following commands to compile and run a standalone MinIO server from source. Source installation is only intended for developers and advanced users. If you do not have a working Golang environment, please follow [How to install Golang](https://golang.org/doc/install). Minimum version required is [go1.17](https://golang.org/dl/#stable)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
GO111MODULE=on go install github.com/minio/minio@latest
|
GO111MODULE=on go install github.com/minio/minio@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to http://127.0.0.1:9000 and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see https://docs.min.io/docs/ and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
You can also connect using any S3-compatible tool, such as the MinIO Client `mc` commandline tool. See [Test using MinIO Client `mc`](#test-using-minio-client-mc) for more information on using the `mc` commandline tool. For application developers, see <https://docs.min.io/docs/> and click **MinIO SDKs** in the navigation to view MinIO SDKs for supported languages.
|
||||||
|
|
||||||
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
> NOTE: Standalone MinIO servers are best suited for early development and evaluation. Certain features such as versioning, object locking, and bucket replication require distributed deploying MinIO with Erasure Coding. For extended development and production, deploy MinIO with Erasure Coding enabled - specifically, with a *minimum* of 4 drives per MinIO server. See [MinIO Erasure Code Quickstart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide.html) for more complete documentation.
|
||||||
|
|
||||||
MinIO strongly recommends *against* using compiled-from-source MinIO servers for production environments.
|
MinIO strongly recommends *against* using compiled-from-source MinIO servers for production environments.
|
||||||
|
|
||||||
# Deployment Recommendations
|
## Deployment Recommendations
|
||||||
|
|
||||||
## Allow port access for Firewalls
|
### Allow port access for Firewalls
|
||||||
|
|
||||||
By default MinIO uses the port 9000 to listen for incoming connections. If your platform blocks the port by default, you may need to enable access to the port.
|
By default MinIO uses the port 9000 to listen for incoming connections. If your platform blocks the port by default, you may need to enable access to the port.
|
||||||
|
|
||||||
@@ -197,19 +196,16 @@ iptables -A INPUT -p tcp --dport 9000:9010 -j ACCEPT
|
|||||||
service iptables restart
|
service iptables restart
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pre-existing data
|
## Test MinIO Connectivity
|
||||||
When deployed on a single drive, MinIO server lets clients access any pre-existing data in the data directory. For example, if MinIO is started with the command `minio server /mnt/data`, any pre-existing data in the `/mnt/data` directory would be accessible to the clients.
|
|
||||||
|
|
||||||
The above statement is also valid for all gateway backends.
|
### Test using MinIO Console
|
||||||
|
|
||||||
# Test MinIO Connectivity
|
MinIO Server comes with an embedded web based object browser. Point your web browser to <http://127.0.0.1:9000> to ensure your server has started successfully.
|
||||||
|
|
||||||
## Test using MinIO Console
|
|
||||||
MinIO Server comes with an embedded web based object browser. Point your web browser to http://127.0.0.1:9000 to ensure your server has started successfully.
|
|
||||||
|
|
||||||
> NOTE: MinIO runs console on random port by default if you wish choose a specific port use `--console-address` to pick a specific interface and port.
|
> NOTE: MinIO runs console on random port by default if you wish choose a specific port use `--console-address` to pick a specific interface and port.
|
||||||
|
|
||||||
### Things to consider
|
### Things to consider
|
||||||
|
|
||||||
MinIO redirects browser access requests to the configured server port (i.e. `127.0.0.1:9000`) to the configured Console port. MinIO uses the hostname or IP address specified in the request when building the redirect URL. The URL and port *must* be accessible by the client for the redirection to work.
|
MinIO redirects browser access requests to the configured server port (i.e. `127.0.0.1:9000`) to the configured Console port. MinIO uses the hostname or IP address specified in the request when building the redirect URL. The URL and port *must* be accessible by the client for the redirection to work.
|
||||||
|
|
||||||
For deployments behind a load balancer, proxy, or ingress rule where the MinIO host IP address or port is not public, use the `MINIO_BROWSER_REDIRECT_URL` environment variable to specify the external hostname for the redirect. The LB/Proxy must have rules for directing traffic to the Console port specifically.
|
For deployments behind a load balancer, proxy, or ingress rule where the MinIO host IP address or port is not public, use the `MINIO_BROWSER_REDIRECT_URL` environment variable to specify the external hostname for the redirect. The LB/Proxy must have rules for directing traffic to the Console port specifically.
|
||||||
@@ -220,34 +216,40 @@ Similarly, if your TLS certificates do not have the IP SAN for the MinIO server
|
|||||||
|
|
||||||
For example: `export MINIO_SERVER_URL="https://minio.example.net"`
|
For example: `export MINIO_SERVER_URL="https://minio.example.net"`
|
||||||
|
|
||||||
|
|
||||||
| Dashboard | Creating a bucket |
|
| Dashboard | Creating a bucket |
|
||||||
| ------------- | ------------- |
|
| ------------- | ------------- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
## Test using MinIO Client `mc`
|
## Test using MinIO Client `mc`
|
||||||
|
|
||||||
`mc` provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services. Follow the MinIO Client [Quickstart Guide](https://docs.min.io/docs/minio-client-quickstart-guide) for further instructions.
|
`mc` provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff etc. It supports filesystems and Amazon S3 compatible cloud storage services. Follow the MinIO Client [Quickstart Guide](https://docs.min.io/docs/minio-client-quickstart-guide) for further instructions.
|
||||||
|
|
||||||
# Upgrading MinIO
|
## Upgrading MinIO
|
||||||
MinIO server supports rolling upgrades, i.e. you can update one MinIO instance at a time in a distributed cluster. This allows upgrades with no downtime. Upgrades can be done manually by replacing the binary with the latest release and restarting all servers in a rolling fashion. However, we recommend all our users to use [`mc admin update`](https://docs.min.io/docs/minio-admin-complete-guide.html#update) from the client. This will update all the nodes in the cluster simultaneously and restart them, as shown in the following command from the MinIO client (mc):
|
|
||||||
|
|
||||||
```
|
Upgrades require zero downtime in MinIO, all upgrades are non-disruptive, all transactions on MinIO are atomic. So upgrading all the servers simultaneously is the recommended way to upgrade MinIO.
|
||||||
|
|
||||||
|
> NOTE: requires internet access to update directly from <https://dl.min.io>, optionally you can host any mirrors at <https://my-artifactory.example.com/minio/>
|
||||||
|
|
||||||
|
- For deployments that installed the MinIO server binary by hand, use [`mc admin update`](https://docs.min.io/minio/baremetal/reference/minio-mc-admin/mc-admin-update.html)
|
||||||
|
|
||||||
|
```sh
|
||||||
mc admin update <minio alias, e.g., myminio>
|
mc admin update <minio alias, e.g., myminio>
|
||||||
```
|
```
|
||||||
|
|
||||||
> NOTE: some releases might not allow rolling upgrades, this is always called out in the release notes and it is generally advised to read release notes before upgrading. In such a situation `mc admin update` is the recommended upgrading mechanism to upgrade all servers at once.
|
- For deployments without external internet access (e.g. airgapped environments), download the binary from <https://dl.min.io> and replace the existing MinIO binary let's say for example `/opt/bin/minio`, apply executable permissions `chmod +x /opt/bin/minio` and proceed to perform `mc admin service restart alias/`.
|
||||||
|
|
||||||
## Important things to remember during MinIO upgrades
|
- For installations using Systemd MinIO service, upgrade via RPM/DEB packages **parallelly** on all servers or replace the binary lets say `/opt/bin/minio` on all nodes, apply executable permissions `chmod +x /opt/bin/minio` and process to perform `mc admin service restart alias/`.
|
||||||
|
|
||||||
- `mc admin update` will only work if the user running MinIO has write access to the parent directory where the binary is located, for example if the current binary is at `/usr/local/bin/minio`, you would need write access to `/usr/local/bin`.
|
### Upgrade Checklist
|
||||||
- `mc admin update` updates and restarts all servers simultaneously, applications would retry and continue their respective operations upon upgrade.
|
|
||||||
- `mc admin update` is disabled in kubernetes/container environments, container environments provide their own mechanisms to rollout of updates.
|
- Test all upgrades in a lower environment (DEV, QA, UAT) before applying to production. Performing blind upgrades in production environments carries significant risk.
|
||||||
- In the case of federated setups `mc admin update` should be run against each cluster individually. Avoid updating `mc` to any new releases until all clusters have been successfully updated.
|
- Read the release notes for MinIO *before* performing any upgrade, there is no forced requirement to upgrade to latest releases upon every releases. Some releases may not be relevant to your setup, avoid upgrading production environments unnecessarily.
|
||||||
- If using `kes` as KMS with MinIO, just replace the binary and restart `kes` more information about `kes` can be found [here](https://github.com/minio/kes/wiki)
|
- If you plan to use `mc admin update`, MinIO process must have write access to the parent directory where the binary is present on the host system.
|
||||||
- If using Vault as KMS with MinIO, ensure you have followed the Vault upgrade procedure outlined here: https://www.vaultproject.io/docs/upgrading/index.html
|
- `mc admin update` is not supported and should be avoided in kubernetes/container environments, please upgrade containers by upgrading relevant container images.
|
||||||
- If using etcd with MinIO for the federation, ensure you have followed the etcd upgrade procedure outlined here: https://github.com/etcd-io/etcd/blob/master/Documentation/upgrades/upgrading-etcd.md
|
- **We do not recommend upgrading one MinIO server at a time, the product is designed to support parallel upgrades please follow our recommended guidelines.**
|
||||||
|
|
||||||
|
## Explore Further
|
||||||
|
|
||||||
# Explore Further
|
|
||||||
- [MinIO Erasure Code QuickStart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide)
|
- [MinIO Erasure Code QuickStart Guide](https://docs.min.io/docs/minio-erasure-code-quickstart-guide)
|
||||||
- [Use `mc` with MinIO Server](https://docs.min.io/docs/minio-client-quickstart-guide)
|
- [Use `mc` with MinIO Server](https://docs.min.io/docs/minio-client-quickstart-guide)
|
||||||
- [Use `aws-cli` with MinIO Server](https://docs.min.io/docs/aws-cli-with-minio)
|
- [Use `aws-cli` with MinIO Server](https://docs.min.io/docs/aws-cli-with-minio)
|
||||||
@@ -255,9 +257,12 @@ mc admin update <minio alias, e.g., myminio>
|
|||||||
- [Use `minio-go` SDK with MinIO Server](https://docs.min.io/docs/golang-client-quickstart-guide)
|
- [Use `minio-go` SDK with MinIO Server](https://docs.min.io/docs/golang-client-quickstart-guide)
|
||||||
- [The MinIO documentation website](https://docs.min.io)
|
- [The MinIO documentation website](https://docs.min.io)
|
||||||
|
|
||||||
# Contribute to MinIO Project
|
## Contribute to MinIO Project
|
||||||
|
|
||||||
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
Please follow MinIO [Contributor's Guide](https://github.com/minio/minio/blob/master/CONTRIBUTING.md)
|
||||||
|
|
||||||
# License
|
## License
|
||||||
MinIO source is licensed under the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
|
|
||||||
MinIO [Documentation](https://github.com/minio/minio/tree/master/docs) © 2021 by MinIO, Inc is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
- MinIO source is licensed under the GNU AGPLv3 license that can be found in the [LICENSE](https://github.com/minio/minio/blob/master/LICENSE) file.
|
||||||
|
- MinIO [Documentation](https://github.com/minio/minio/tree/master/docs) © 2021 by MinIO, Inc is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
|
||||||
|
- [License Compliance](https://github.com/minio/minio/blob/master/COMPLIANCE.md)
|
||||||
|
|||||||
+5
-4
@@ -18,9 +18,10 @@ you need access credentials for a successful exploit).
|
|||||||
|
|
||||||
If you have not received a reply to your email within 48 hours or you have not heard from the security team
|
If you have not received a reply to your email within 48 hours or you have not heard from the security team
|
||||||
for the past five days please contact the security team directly:
|
for the past five days please contact the security team directly:
|
||||||
- Primary security coordinator: aead@min.io
|
|
||||||
- Secondary coordinator: harsha@min.io
|
- Primary security coordinator: aead@min.io
|
||||||
- If you receive no response: dev@min.io
|
- Secondary coordinator: harsha@min.io
|
||||||
|
- If you receive no response: dev@min.io
|
||||||
|
|
||||||
### Disclosure Process
|
### Disclosure Process
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ MinIO uses the following disclosure process:
|
|||||||
If the report is rejected the response explains why.
|
If the report is rejected the response explains why.
|
||||||
3. Code is audited to find any potential similar problems.
|
3. Code is audited to find any potential similar problems.
|
||||||
4. Fixes are prepared for the latest release.
|
4. Fixes are prepared for the latest release.
|
||||||
5. On the date that the fixes are applied a security advisory will be published on https://blog.min.io.
|
5. On the date that the fixes are applied a security advisory will be published on <https://blog.min.io>.
|
||||||
Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing
|
Please inform us in your report email whether MinIO should mention your contribution w.r.t. fixing
|
||||||
the security issue. By default MinIO will **not** publish this information to protect your privacy.
|
the security issue. By default MinIO will **not** publish this information to protect your privacy.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
## Vulnerability Management Policy
|
# Vulnerability Management Policy
|
||||||
|
|
||||||
This document formally describes the process of addressing and managing a
|
This document formally describes the process of addressing and managing a
|
||||||
reported vulnerability that has been found in the MinIO server code base,
|
reported vulnerability that has been found in the MinIO server code base,
|
||||||
any directly connected ecosystem component or a direct / indirect dependency
|
any directly connected ecosystem component or a direct / indirect dependency
|
||||||
of the code base.
|
of the code base.
|
||||||
|
|
||||||
### Scope
|
## Scope
|
||||||
|
|
||||||
The vulnerability management policy described in this document covers the
|
The vulnerability management policy described in this document covers the
|
||||||
process of investigating, assessing and resolving a vulnerability report
|
process of investigating, assessing and resolving a vulnerability report
|
||||||
@@ -14,13 +14,13 @@ opened by a MinIO employee or an external third party.
|
|||||||
Therefore, it lists pre-conditions and actions that should be performed to
|
Therefore, it lists pre-conditions and actions that should be performed to
|
||||||
resolve and fix a reported vulnerability.
|
resolve and fix a reported vulnerability.
|
||||||
|
|
||||||
### Vulnerability Management Process
|
## Vulnerability Management Process
|
||||||
|
|
||||||
The vulnerability management process requires that the vulnerability report
|
The vulnerability management process requires that the vulnerability report
|
||||||
contains the following information:
|
contains the following information:
|
||||||
|
|
||||||
- The project / component that contains the reported vulnerability.
|
- The project / component that contains the reported vulnerability.
|
||||||
- A description of the vulnerability. In particular, the type of the
|
- A description of the vulnerability. In particular, the type of the
|
||||||
reported vulnerability and how it might be exploited. Alternatively,
|
reported vulnerability and how it might be exploited. Alternatively,
|
||||||
a well-established vulnerability identifier, e.g. CVE number, can be
|
a well-established vulnerability identifier, e.g. CVE number, can be
|
||||||
used instead.
|
used instead.
|
||||||
@@ -28,12 +28,11 @@ contains the following information:
|
|||||||
Based on the description mentioned above, a MinIO engineer or security team
|
Based on the description mentioned above, a MinIO engineer or security team
|
||||||
member investigates:
|
member investigates:
|
||||||
|
|
||||||
- Whether the reported vulnerability exists.
|
- Whether the reported vulnerability exists.
|
||||||
- The conditions that are required such that the vulnerability can be exploited.
|
- The conditions that are required such that the vulnerability can be exploited.
|
||||||
- The steps required to fix the vulnerability.
|
- The steps required to fix the vulnerability.
|
||||||
|
|
||||||
In general, if the vulnerability exists in one of the MinIO code bases
|
In general, if the vulnerability exists in one of the MinIO code bases
|
||||||
itself - not in a code dependency - then MinIO will, if possible, fix
|
itself - not in a code dependency - then MinIO will, if possible, fix
|
||||||
the vulnerability or implement reasonable countermeasures such that the
|
the vulnerability or implement reasonable countermeasures such that the
|
||||||
vulnerability cannot be exploited anymore.
|
vulnerability cannot be exploited anymore.
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
**/*.swp
|
|
||||||
cover.out
|
|
||||||
*~
|
|
||||||
minio
|
|
||||||
!*/
|
|
||||||
site/
|
|
||||||
**/*.test
|
|
||||||
**/*.sublime-workspace
|
|
||||||
/.idea/
|
|
||||||
/Minio.iml
|
|
||||||
**/access.log
|
|
||||||
build
|
|
||||||
vendor/**/*.js
|
|
||||||
vendor/**/*.json
|
|
||||||
.DS_Store
|
|
||||||
*.syso
|
|
||||||
coverage.txt
|
|
||||||
node_modules
|
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -24,14 +24,18 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func genLDFlags(version string) string {
|
func genLDFlags(version string) string {
|
||||||
|
releaseTag, date := releaseTag(version)
|
||||||
|
copyrightYear := strconv.Itoa(date.Year())
|
||||||
ldflagsStr := "-s -w"
|
ldflagsStr := "-s -w"
|
||||||
ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
|
ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
|
||||||
ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag(version)
|
ldflagsStr += " -X github.com/minio/minio/cmd.CopyrightYear=" + copyrightYear
|
||||||
|
ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag
|
||||||
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
||||||
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
||||||
ldflagsStr += " -X github.com/minio/minio/cmd.GOPATH=" + os.Getenv("GOPATH")
|
ldflagsStr += " -X github.com/minio/minio/cmd.GOPATH=" + os.Getenv("GOPATH")
|
||||||
@@ -40,7 +44,7 @@ func genLDFlags(version string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// genReleaseTag prints release tag to the console for easy git tagging.
|
// genReleaseTag prints release tag to the console for easy git tagging.
|
||||||
func releaseTag(version string) string {
|
func releaseTag(version string) (string, time.Time) {
|
||||||
relPrefix := "DEVELOPMENT"
|
relPrefix := "DEVELOPMENT"
|
||||||
if prefix := os.Getenv("MINIO_RELEASE"); prefix != "" {
|
if prefix := os.Getenv("MINIO_RELEASE"); prefix != "" {
|
||||||
relPrefix = prefix
|
relPrefix = prefix
|
||||||
@@ -53,14 +57,17 @@ func releaseTag(version string) string {
|
|||||||
|
|
||||||
relTag := strings.Replace(version, " ", "-", -1)
|
relTag := strings.Replace(version, " ", "-", -1)
|
||||||
relTag = strings.Replace(relTag, ":", "-", -1)
|
relTag = strings.Replace(relTag, ":", "-", -1)
|
||||||
|
t, err := time.Parse("2006-01-02T15-04-05Z", relTag)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
relTag = strings.Replace(relTag, ",", "", -1)
|
relTag = strings.Replace(relTag, ",", "", -1)
|
||||||
relTag = relPrefix + "." + relTag
|
relTag = relPrefix + "." + relTag
|
||||||
|
|
||||||
if relSuffix != "" {
|
if relSuffix != "" {
|
||||||
relTag += "." + relSuffix
|
relTag += "." + relSuffix
|
||||||
}
|
}
|
||||||
|
|
||||||
return relTag
|
return relTag, t
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitID returns the abbreviated commit-id hash of the last commit.
|
// commitID returns the abbreviated commit-id hash of the last commit.
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
trap 'cleanup $LINENO' ERR
|
||||||
|
|
||||||
|
# shellcheck disable=SC2120
|
||||||
|
cleanup() {
|
||||||
|
MINIO_VERSION=dev docker-compose \
|
||||||
|
-f "buildscripts/upgrade-tests/compose.yml" \
|
||||||
|
rm -s -f
|
||||||
|
docker volume prune -f
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_checksum_after_heal() {
|
||||||
|
local sum1
|
||||||
|
sum1=$(curl -s "$2" | sha256sum);
|
||||||
|
mc admin heal --json -r "$1" >/dev/null; # test after healing
|
||||||
|
local sum1_heal
|
||||||
|
sum1_heal=$(curl -s "$2" | sha256sum);
|
||||||
|
|
||||||
|
if [ "${sum1_heal}" != "${sum1}" ]; then
|
||||||
|
echo "mismatch expected ${sum1_heal}, got ${sum1}"
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_checksum_mc() {
|
||||||
|
local expected
|
||||||
|
expected=$(mc cat "$1" | sha256sum)
|
||||||
|
local got
|
||||||
|
got=$(mc cat "$2" | sha256sum)
|
||||||
|
|
||||||
|
if [ "${expected}" != "${got}" ]; then
|
||||||
|
echo "mismatch - expected ${expected}, got ${got}"
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
echo "matches - ${expected}, got ${got}"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_alias() {
|
||||||
|
for i in $(seq 1 4); do
|
||||||
|
echo "... attempting to add alias $i"
|
||||||
|
until (mc alias set minio http://127.0.0.1:9000 minioadmin minioadmin); do
|
||||||
|
echo "...waiting... for 5secs" && sleep 5
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Sleeping for nginx"
|
||||||
|
sleep 20
|
||||||
|
}
|
||||||
|
|
||||||
|
__init__() {
|
||||||
|
sudo apt install curl -y
|
||||||
|
export GOPATH=/tmp/gopath
|
||||||
|
export PATH=${PATH}:${GOPATH}/bin
|
||||||
|
|
||||||
|
go install github.com/minio/mc@latest
|
||||||
|
|
||||||
|
TAG=minio/minio:dev make docker
|
||||||
|
|
||||||
|
MINIO_VERSION=RELEASE.2019-12-19T22-52-26Z docker-compose \
|
||||||
|
-f "buildscripts/upgrade-tests/compose.yml" \
|
||||||
|
up -d --build
|
||||||
|
|
||||||
|
add_alias
|
||||||
|
|
||||||
|
mc mb minio/minio-test/
|
||||||
|
mc cp ./minio minio/minio-test/to-read/
|
||||||
|
mc cp /etc/hosts minio/minio-test/to-read/hosts
|
||||||
|
mc policy set download minio/minio-test
|
||||||
|
|
||||||
|
verify_checksum_mc ./minio minio/minio-test/to-read/minio
|
||||||
|
|
||||||
|
curl -s http://127.0.0.1:9000/minio-test/to-read/hosts | sha256sum
|
||||||
|
|
||||||
|
MINIO_VERSION=dev docker-compose -f "buildscripts/upgrade-tests/compose.yml" stop
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
MINIO_VERSION=dev docker-compose -f "buildscripts/upgrade-tests/compose.yml" up -d --build
|
||||||
|
|
||||||
|
add_alias
|
||||||
|
|
||||||
|
verify_checksum_after_heal minio/minio-test http://127.0.0.1:9000/minio-test/to-read/hosts
|
||||||
|
|
||||||
|
verify_checksum_mc ./minio minio/minio-test/to-read/minio
|
||||||
|
|
||||||
|
verify_checksum_mc /etc/hosts minio/minio-test/to-read/hosts
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
( __init__ "$@" && main "$@" )
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
for d in $(go list ./... | grep -v browser); do
|
export GORACE="history_size=7"
|
||||||
CGO_ENABLED=1 go test -v -tags kqueue -race --timeout 100m "$d"
|
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
||||||
|
for d in $(go list ./... | grep -v dsync); do
|
||||||
|
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||||
done
|
done
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
set -E
|
||||||
|
set -o pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||||
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
function start_minio_5drive() {
|
||||||
|
start_port=$1
|
||||||
|
|
||||||
|
export MINIO_ROOT_USER=minio
|
||||||
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
|
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
|
||||||
|
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
|
MC_BUILD_DIR="mc-$RANDOM"
|
||||||
|
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
|
||||||
|
echo "failed to download https://github.com/minio/mc"
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "${MC_BUILD_DIR}" && go build -o "$WORK_DIR/mc")
|
||||||
|
|
||||||
|
# remove mc source.
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" cp --quiet -r "buildscripts/cicd-corpus/" "${WORK_DIR}/cicd-corpus/"
|
||||||
|
|
||||||
|
"${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/cicd-corpus/disk{1...5}" > "${WORK_DIR}/server1.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown $pid
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" stat minio/bucket/testobj
|
||||||
|
|
||||||
|
pkill minio
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
|
|
||||||
|
start_minio_5drive ${start_port}
|
||||||
|
}
|
||||||
|
|
||||||
|
function purge()
|
||||||
|
{
|
||||||
|
rm -rf "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
( main "$@" )
|
||||||
|
rv=$?
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit "$rv"
|
||||||
Executable
+172
@@ -0,0 +1,172 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
#
|
||||||
|
|
||||||
|
set -E
|
||||||
|
set -o pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||||
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
|
MINIO_OLD=( "$PWD/minio.RELEASE.2021-11-24T23-19-33Z" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
|
||||||
|
function download_old_release() {
|
||||||
|
if [ ! -f minio.RELEASE.2021-11-24T23-19-33Z ]; then
|
||||||
|
curl --silent -O https://dl.minio.io/server/minio/release/linux-amd64/archive/minio.RELEASE.2021-11-24T23-19-33Z
|
||||||
|
chmod a+x minio.RELEASE.2021-11-24T23-19-33Z
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function start_minio_16drive() {
|
||||||
|
start_port=$1
|
||||||
|
|
||||||
|
export MINIO_ROOT_USER=minio
|
||||||
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
|
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
|
||||||
|
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||||
|
export _MINIO_SHARD_DISKTIME_DELTA="5s" # do not change this as its needed for tests
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
|
MC_BUILD_DIR="mc-$RANDOM"
|
||||||
|
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
|
||||||
|
echo "failed to download https://github.com/minio/mc"
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "${MC_BUILD_DIR}" && go build -o "$WORK_DIR/mc")
|
||||||
|
|
||||||
|
# remove mc source.
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
|
||||||
|
"${MINIO_OLD[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" > "${WORK_DIR}/server1.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown $pid
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shred --iterations=1 --size=5241856 - 1>"${WORK_DIR}/unaligned" 2>/dev/null
|
||||||
|
"${WORK_DIR}/mc" mb minio/healing-shard-bucket --quiet
|
||||||
|
"${WORK_DIR}/mc" cp \
|
||||||
|
"${WORK_DIR}/unaligned" \
|
||||||
|
minio/healing-shard-bucket/unaligned \
|
||||||
|
--disable-multipart --quiet
|
||||||
|
|
||||||
|
## "unaligned" object name gets consistently distributed
|
||||||
|
## to disks in following distribution order
|
||||||
|
##
|
||||||
|
## NOTE: if you change the name make sure to change the
|
||||||
|
## distribution order present here
|
||||||
|
##
|
||||||
|
## [15, 16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
|
||||||
|
|
||||||
|
## make sure to remove the "last" data shard
|
||||||
|
rm -rf "${WORK_DIR}/xl14/healing-shard-bucket/unaligned"
|
||||||
|
sleep 10
|
||||||
|
## Heal the shard
|
||||||
|
"${WORK_DIR}/mc" admin heal --quiet --recursive minio/healing-shard-bucket
|
||||||
|
## then remove any other data shard let's pick first disk
|
||||||
|
## - 1st data shard.
|
||||||
|
rm -rf "${WORK_DIR}/xl3/healing-shard-bucket/unaligned"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
go build ./docs/debugging/s3-check-md5/
|
||||||
|
if ! ./s3-check-md5 \
|
||||||
|
-debug \
|
||||||
|
-access-key minio \
|
||||||
|
-secret-key minio123 \
|
||||||
|
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep CORRUPTED; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkill minio
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
"${MINIO[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" > "${WORK_DIR}/server1.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown $pid
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ./s3-check-md5 \
|
||||||
|
-debug \
|
||||||
|
-access-key minio \
|
||||||
|
-secret-key minio123 \
|
||||||
|
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
mkdir -p inspects
|
||||||
|
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-shard-bucket/unaligned/**)
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" mb play/inspects
|
||||||
|
"${WORK_DIR}/mc" mirror inspects play/inspects
|
||||||
|
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" admin heal --quiet --recursive minio/healing-shard-bucket
|
||||||
|
|
||||||
|
if ! ./s3-check-md5 \
|
||||||
|
-debug \
|
||||||
|
-access-key minio \
|
||||||
|
-secret-key minio123 \
|
||||||
|
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
mkdir -p inspects
|
||||||
|
(cd inspects; "${WORK_DIR}/mc" admin inspect minio/healing-shard-bucket/unaligned/**)
|
||||||
|
|
||||||
|
"${WORK_DIR}/mc" mb play/inspects
|
||||||
|
"${WORK_DIR}/mc" mirror inspects play/inspects
|
||||||
|
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkill minio
|
||||||
|
sleep 3
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
download_old_release
|
||||||
|
|
||||||
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
|
|
||||||
|
start_minio_16drive ${start_port}
|
||||||
|
}
|
||||||
|
|
||||||
|
function purge()
|
||||||
|
{
|
||||||
|
rm -rf "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
( main "$@" )
|
||||||
|
rv=$?
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit "$rv"
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
# Settings and configurations that are common for all containers
|
||||||
|
x-minio-common: &minio-common
|
||||||
|
image: minio/minio:${MINIO_VERSION}
|
||||||
|
command: server http://minio{1...4}/data{1...3}
|
||||||
|
env_file:
|
||||||
|
- ./minio.env
|
||||||
|
expose:
|
||||||
|
- "9000"
|
||||||
|
- "9001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 20s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# starts 4 docker containers running minio server instances.
|
||||||
|
# using nginx reverse proxy, load balancing, you can access
|
||||||
|
# it through port 9000.
|
||||||
|
services:
|
||||||
|
minio1:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio1
|
||||||
|
volumes:
|
||||||
|
- data1-1:/data1
|
||||||
|
- data1-2:/data2
|
||||||
|
- data1-3:/data3
|
||||||
|
|
||||||
|
minio2:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio2
|
||||||
|
volumes:
|
||||||
|
- data2-1:/data1
|
||||||
|
- data2-2:/data2
|
||||||
|
- data2-3:/data3
|
||||||
|
|
||||||
|
minio3:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio3
|
||||||
|
volumes:
|
||||||
|
- data3-1:/data1
|
||||||
|
- data3-2:/data2
|
||||||
|
- data3-3:/data3
|
||||||
|
|
||||||
|
minio4:
|
||||||
|
<<: *minio-common
|
||||||
|
hostname: minio4
|
||||||
|
volumes:
|
||||||
|
- data4-1:/data1
|
||||||
|
- data4-2:/data2
|
||||||
|
- data4-3:/data3
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.19.2-alpine
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
depends_on:
|
||||||
|
- minio1
|
||||||
|
- minio2
|
||||||
|
- minio3
|
||||||
|
- minio4
|
||||||
|
|
||||||
|
## By default this config uses default local driver,
|
||||||
|
## For custom volumes replace with volume driver configuration.
|
||||||
|
volumes:
|
||||||
|
data1-1:
|
||||||
|
data1-2:
|
||||||
|
data1-3:
|
||||||
|
data2-1:
|
||||||
|
data2-2:
|
||||||
|
data2-3:
|
||||||
|
data3-1:
|
||||||
|
data3-2:
|
||||||
|
data3-3:
|
||||||
|
data4-1:
|
||||||
|
data4-2:
|
||||||
|
data4-3:
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
MINIO_BROWSER=off
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
#tcp_nopush on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
#gzip on;
|
||||||
|
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
|
upstream minio {
|
||||||
|
server minio1:9000;
|
||||||
|
server minio2:9000;
|
||||||
|
server minio3:9000;
|
||||||
|
server minio4:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# main minio
|
||||||
|
server {
|
||||||
|
listen 9000;
|
||||||
|
listen [::]:9000;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# To allow special characters in headers
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
# Allow any size file to be uploaded.
|
||||||
|
# Set to a value such as 1000m; to restrict file size to a specific value
|
||||||
|
client_max_body_size 0;
|
||||||
|
# To disable buffering
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
|
||||||
|
proxy_pass http://minio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,9 @@ export SECRET_KEY="minio123"
|
|||||||
export ENABLE_HTTPS=0
|
export ENABLE_HTTPS=0
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
export GOGC=25
|
export GOGC=25
|
||||||
|
export ENABLE_ADMIN=1
|
||||||
|
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
|
||||||
@@ -65,7 +68,7 @@ function start_minio_pool_erasure_sets_ipv6()
|
|||||||
{
|
{
|
||||||
export MINIO_ROOT_USER=$ACCESS_KEY
|
export MINIO_ROOT_USER=$ACCESS_KEY
|
||||||
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
||||||
export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets{5...8}"
|
export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets-ipv6{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets-ipv6{5...8}"
|
||||||
"${MINIO[@]}" server --address="[::1]:9000" > "$WORK_DIR/pool-minio-ipv6-9000.log" 2>&1 &
|
"${MINIO[@]}" server --address="[::1]:9000" > "$WORK_DIR/pool-minio-ipv6-9000.log" 2>&1 &
|
||||||
"${MINIO[@]}" server --address="[::1]:9001" > "$WORK_DIR/pool-minio-ipv6-9001.log" 2>&1 &
|
"${MINIO[@]}" server --address="[::1]:9001" > "$WORK_DIR/pool-minio-ipv6-9001.log" 2>&1 &
|
||||||
|
|
||||||
|
|||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
set -E
|
||||||
|
set -o pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
|
||||||
|
|
||||||
|
function start_minio() {
|
||||||
|
start_port=$1
|
||||||
|
|
||||||
|
export MINIO_ROOT_USER=minio
|
||||||
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
|
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||||
|
unset MINIO_CI_CD
|
||||||
|
unset CI
|
||||||
|
|
||||||
|
args=()
|
||||||
|
for i in $(seq 1 4); do
|
||||||
|
args+=("http://localhost:$[${start_port}+$i]${WORK_DIR}/mnt/disk$i/ ")
|
||||||
|
done
|
||||||
|
|
||||||
|
for i in $(seq 1 4); do
|
||||||
|
"${MINIO[@]}" --address ":$[$start_port+$i]" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" &
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait until all nodes return 403
|
||||||
|
for i in $(seq 1 4); do
|
||||||
|
while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$[$start_port+$i])" -ne "403" ]; do
|
||||||
|
echo -n ".";
|
||||||
|
sleep 1;
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prepare fake disks with losetup
|
||||||
|
function prepare_block_devices() {
|
||||||
|
mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
|
||||||
|
for i in 1 2 3 4; do
|
||||||
|
dd if=/dev/zero of=${WORK_DIR}/disks/img.$i bs=1M count=2048
|
||||||
|
mkfs.ext4 -F ${WORK_DIR}/disks/img.$i
|
||||||
|
sudo mknod /dev/minio-loopdisk$i b 7 $[256-$i]
|
||||||
|
sudo losetup /dev/minio-loopdisk$i ${WORK_DIR}/disks/img.$i
|
||||||
|
mkdir -p ${WORK_DIR}/mnt/disk$i/
|
||||||
|
sudo mount /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
||||||
|
sudo chown "$(id -u):$(id -g)" /dev/minio-loopdisk$i ${WORK_DIR}/mnt/disk$i/
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start a distributed MinIO setup, unmount one disk and check if it is formatted
|
||||||
|
function main() {
|
||||||
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
|
start_minio ${start_port}
|
||||||
|
|
||||||
|
# Unmount the disk, after the unmount the device id
|
||||||
|
# /tmp/xxx/mnt/disk4 will be the same as '/' and it
|
||||||
|
# will be detected as root disk
|
||||||
|
while [ "$u" != "0" ]; do
|
||||||
|
sudo umount ${WORK_DIR}/mnt/disk4/
|
||||||
|
u=$?
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait until MinIO self heal kicks in
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
if [ -f ${WORK_DIR}/mnt/disk4/.minio.sys/format.json ]; then
|
||||||
|
echo "A root disk is formatted unexpectedely"
|
||||||
|
cat "${WORK_DIR}/server4.log"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
pkill minio
|
||||||
|
sudo umount ${WORK_DIR}/mnt/disk{1..3}/
|
||||||
|
sudo rm /dev/minio-loopdisk*
|
||||||
|
rm -rf "$WORK_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
( prepare_block_devices )
|
||||||
|
( main "$@" )
|
||||||
|
rv=$?
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
exit "$rv"
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash -e
|
||||||
#
|
#
|
||||||
|
|
||||||
set -e
|
|
||||||
set -E
|
set -E
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
@@ -14,61 +13,79 @@ WORK_DIR="$PWD/.verify-$RANDOM"
|
|||||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||||
|
|
||||||
export GOGC=25
|
|
||||||
|
|
||||||
function start_minio_3_node() {
|
function start_minio_3_node() {
|
||||||
export MINIO_ROOT_USER=minio
|
export MINIO_ROOT_USER=minio
|
||||||
export MINIO_ROOT_PASSWORD=minio123
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
start_port=$(shuf -i 10000-65000 -n 1)
|
start_port=$2
|
||||||
args=""
|
args=""
|
||||||
for i in $(seq 1 3); do
|
for i in $(seq 1 3); do
|
||||||
args="$args http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/6/"
|
args="$args http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/6/"
|
||||||
done
|
done
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":$[$start_port+1]" $args > "${WORK_DIR}/dist-minio-server1.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+1]" $args > "${WORK_DIR}/dist-minio-server1.log" 2>&1 &
|
||||||
disown $!
|
pid1=$!
|
||||||
|
disown ${pid1}
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":$[$start_port+2]" $args > "${WORK_DIR}/dist-minio-server2.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+2]" $args > "${WORK_DIR}/dist-minio-server2.log" 2>&1 &
|
||||||
disown $!
|
pid2=$!
|
||||||
|
disown $pid2
|
||||||
|
|
||||||
"${MINIO[@]}" --address ":$[$start_port+3]" $args > "${WORK_DIR}/dist-minio-server3.log" 2>&1 &
|
"${MINIO[@]}" --address ":$[$start_port+3]" $args > "${WORK_DIR}/dist-minio-server3.log" 2>&1 &
|
||||||
disown $!
|
pid3=$!
|
||||||
|
disown $pid3
|
||||||
|
|
||||||
sleep "$1"
|
sleep "$1"
|
||||||
if [ "$(pgrep -c minio)" -ne 3 ]; then
|
|
||||||
for i in $(seq 1 3); do
|
if ! ps -p $pid1 1>&2 > /dev/null; then
|
||||||
echo "server$i log:"
|
echo "server1 log:"
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
cat "${WORK_DIR}/dist-minio-server1.log"
|
||||||
done
|
echo "FAILED"
|
||||||
echo "FAILED"
|
purge "$WORK_DIR"
|
||||||
purge "$WORK_DIR"
|
exit 1
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! ps -p $pid2 1>&2 > /dev/null; then
|
||||||
|
echo "server2 log:"
|
||||||
|
cat "${WORK_DIR}/dist-minio-server2.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! ps -p $pid3 1>&2 > /dev/null; then
|
||||||
|
echo "server3 log:"
|
||||||
|
cat "${WORK_DIR}/dist-minio-server3.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! pkill minio; then
|
if ! pkill minio; then
|
||||||
for i in $(seq 1 3); do
|
for i in $(seq 1 3); do
|
||||||
echo "server$i log:"
|
echo "server$i log:"
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
done
|
done
|
||||||
echo "FAILED"
|
echo "FAILED"
|
||||||
purge "$WORK_DIR"
|
purge "$WORK_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 1;
|
sleep 1;
|
||||||
if pgrep minio; then
|
if pgrep minio; then
|
||||||
# forcibly killing, to proceed further properly.
|
# forcibly killing, to proceed further properly.
|
||||||
if ! pkill -9 minio; then
|
if ! pkill -9 minio; then
|
||||||
echo "no minio process running anymore, proceed."
|
echo "no minio process running anymore, proceed."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function check_online() {
|
function check_online() {
|
||||||
if grep -q 'Unable to initialize sub-systems' ${WORK_DIR}/dist-minio-*.log; then
|
if grep -q 'Unable to initialize sub-systems' ${WORK_DIR}/dist-minio-*.log; then
|
||||||
echo "1"
|
echo "1"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,33 +105,36 @@ function __init__()
|
|||||||
}
|
}
|
||||||
|
|
||||||
function perform_test() {
|
function perform_test() {
|
||||||
start_minio_3_node 60
|
start_minio_3_node 120 $2
|
||||||
|
|
||||||
echo "Testing Distributed Erasure setup healing of drives"
|
echo "Testing Distributed Erasure setup healing of drives"
|
||||||
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
||||||
|
|
||||||
rm -rf ${WORK_DIR}/${1}/*/
|
rm -rf ${WORK_DIR}/${1}/*/
|
||||||
|
|
||||||
start_minio_3_node 60
|
start_minio_3_node 120 $2
|
||||||
|
|
||||||
rv=$(check_online)
|
rv=$(check_online)
|
||||||
if [ "$rv" == "1" ]; then
|
if [ "$rv" == "1" ]; then
|
||||||
pkill -9 minio
|
for i in $(seq 1 3); do
|
||||||
for i in $(seq 1 3); do
|
echo "server$i log:"
|
||||||
echo "server$i log:"
|
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
done
|
||||||
done
|
pkill -9 minio
|
||||||
echo "FAILED"
|
echo "FAILED"
|
||||||
purge "$WORK_DIR"
|
purge "$WORK_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
function main()
|
function main()
|
||||||
{
|
{
|
||||||
perform_test "2"
|
# use same ports for all tests
|
||||||
perform_test "1"
|
start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
perform_test "3"
|
|
||||||
|
perform_test "2" ${start_port}
|
||||||
|
perform_test "1" ${start_port}
|
||||||
|
perform_test "3" ${start_port}
|
||||||
}
|
}
|
||||||
|
|
||||||
( __init__ "$@" && main "$@" )
|
( __init__ "$@" && main "$@" )
|
||||||
|
|||||||
@@ -114,8 +114,6 @@ func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketACLHandler - GET Bucket ACL
|
// GetBucketACLHandler - GET Bucket ACL
|
||||||
@@ -164,8 +162,6 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutObjectACLHandler - PUT Object ACL
|
// PutObjectACLHandler - PUT Object ACL
|
||||||
@@ -229,8 +225,6 @@ func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectACLHandler - GET Object ACL
|
// GetObjectACLHandler - GET Object ACL
|
||||||
@@ -283,6 +277,4 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,31 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
"github.com/klauspost/compress/zip"
|
||||||
|
"github.com/minio/kes"
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
|
"github.com/minio/minio/internal/event"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/bucket/policy"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,12 +81,35 @@ func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = parseBucketQuota(bucket, data); err != nil {
|
quotaConfig, err := parseBucketQuota(bucket, data)
|
||||||
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketQuotaConfigFile, data); err != nil {
|
if quotaConfig.Type == "fifo" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketMeta := madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
Quota: data,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}
|
||||||
|
if quotaConfig.Quota == 0 {
|
||||||
|
bucketMeta.Quota = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -99,7 +138,7 @@ func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetQuotaConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -124,7 +163,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
update := r.Form.Get("update") == "true"
|
update := r.Form.Get("update") == "true"
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -155,7 +194,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var target madmin.BucketTarget
|
var target madmin.BucketTarget
|
||||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
if err = json.Unmarshal(reqBytes, &target); err != nil {
|
if err = json.Unmarshal(reqBytes, &target); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -230,7 +269,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketTargetsFile, tgtBytes); err != nil {
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -253,7 +292,8 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
|
|||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
arnType := vars["type"]
|
arnType := vars["type"]
|
||||||
if !globalIsErasure {
|
|
||||||
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -293,7 +333,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
|||||||
bucket := pathClean(vars["bucket"])
|
bucket := pathClean(vars["bucket"])
|
||||||
arn := vars["arn"]
|
arn := vars["arn"]
|
||||||
|
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -324,7 +364,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketTargetsFile, tgtBytes); err != nil {
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketTargetsFile, tgtBytes); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -332,3 +372,698 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
|||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportBucketMetadataHandler - exports all bucket metadata as a zipped file
|
||||||
|
func (a adminAPIHandlers) ExportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ExportBucketMetadata")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
bucket := pathClean(r.Form.Get("bucket"))
|
||||||
|
if !globalIsErasure {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ExportBucketMetadataAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
buckets []BucketInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if bucket != "" {
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buckets = append(buckets, BucketInfo{Name: bucket})
|
||||||
|
} else {
|
||||||
|
buckets, err = objectAPI.ListBuckets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a zip writer which will provide a zipped content
|
||||||
|
// of bucket metadata
|
||||||
|
zipWriter := zip.NewWriter(w)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
rawDataFn := func(r io.Reader, filename string, sz int) error {
|
||||||
|
header, zerr := zip.FileInfoHeader(dummyFileInfo{
|
||||||
|
name: filename,
|
||||||
|
size: int64(sz),
|
||||||
|
mode: 0o600,
|
||||||
|
modTime: time.Now(),
|
||||||
|
isDir: false,
|
||||||
|
sys: nil,
|
||||||
|
})
|
||||||
|
if zerr != nil {
|
||||||
|
logger.LogIf(ctx, zerr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
header.Method = zip.Deflate
|
||||||
|
zwriter, zerr := zipWriter.CreateHeader(header)
|
||||||
|
if zerr != nil {
|
||||||
|
logger.LogIf(ctx, zerr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(zwriter, r); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFiles := []string{
|
||||||
|
bucketPolicyConfig,
|
||||||
|
bucketNotificationConfig,
|
||||||
|
bucketLifecycleConfig,
|
||||||
|
bucketSSEConfig,
|
||||||
|
bucketTaggingConfig,
|
||||||
|
bucketQuotaConfigFile,
|
||||||
|
objectLockConfig,
|
||||||
|
bucketVersioningConfig,
|
||||||
|
bucketReplicationConfig,
|
||||||
|
bucketTargetsFile,
|
||||||
|
}
|
||||||
|
for _, bi := range buckets {
|
||||||
|
for _, cfgFile := range cfgFiles {
|
||||||
|
cfgPath := pathJoin(bi.Name, cfgFile)
|
||||||
|
bucket := bi.Name
|
||||||
|
switch cfgFile {
|
||||||
|
case bucketNotificationConfig:
|
||||||
|
config, err := globalBucketMetadataSys.GetNotificationConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketLifecycleConfig:
|
||||||
|
config, err := globalBucketMetadataSys.GetLifecycleConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketLifecycleNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketQuotaConfigFile:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetQuotaConfig(ctx, bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketQuotaConfigNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketSSEConfig:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketSSEConfigNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketTaggingConfig:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketTaggingNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case objectLockConfig:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketObjectLockConfigNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketVersioningConfig:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ignore empty versioning configs
|
||||||
|
if config.Status != versioning.Enabled && config.Status != versioning.Suspended {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketReplicationConfig:
|
||||||
|
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketReplicationConfigNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketTargetsFile:
|
||||||
|
config, err := globalBucketMetadataSys.GetBucketTargetsConfig(bucket)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, BucketRemoteTargetNotFound{Bucket: bucket}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = rawDataFn(bytes.NewReader(configData), cfgPath, len(configData)); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, exportError(ctx, err, cfgFile, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImportBucketMetadataHandler - imports all bucket metadata from a zipped file and overwrite bucket metadata config
|
||||||
|
// There are some caveats regarding the following:
|
||||||
|
// 1. object lock config - object lock should have been specified at time of bucket creation. Only default retention settings are imported here.
|
||||||
|
// 2. Replication config - is omitted from import as remote target credentials are not available from exported data for security reasons.
|
||||||
|
// 3. lifecycle config - if transition rules are present, tier name needs to have been defined.
|
||||||
|
func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ImportBucketMetadata")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
bucket := pathClean(r.Form.Get("bucket"))
|
||||||
|
|
||||||
|
if !globalIsErasure {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ImportBucketMetadataAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
zr, err := zip.NewReader(reader, int64(len(data)))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bucketMap := make(map[string]struct{}, 1)
|
||||||
|
|
||||||
|
// import object lock config if any - order of import matters here.
|
||||||
|
for _, file := range zr.File {
|
||||||
|
slc := strings.Split(file.Name, slashSeparator)
|
||||||
|
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, fileName := slc[0], slc[1]
|
||||||
|
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||||
|
bucket = b
|
||||||
|
}
|
||||||
|
switch fileName {
|
||||||
|
case objectLockConfig:
|
||||||
|
reader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config, err := objectlock.ParseObjectLockConfig(reader)
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
|
apiErr.Description = err.Error()
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := bucketMap[bucket]; !ok {
|
||||||
|
opts := BucketOptions{
|
||||||
|
LockEnabled: config.ObjectLockEnabled == "Enabled",
|
||||||
|
}
|
||||||
|
err = objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(BucketExists); !ok {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucketMap[bucket] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||||
|
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
ObjectLockConfig: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// import versioning metadata
|
||||||
|
for _, file := range zr.File {
|
||||||
|
slc := strings.Split(file.Name, slashSeparator)
|
||||||
|
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, fileName := slc[0], slc[1]
|
||||||
|
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||||
|
bucket = b
|
||||||
|
}
|
||||||
|
switch fileName {
|
||||||
|
case bucketVersioningConfig:
|
||||||
|
reader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v, err := versioning.ParseConfig(io.LimitReader(reader, maxBucketVersioningConfigSize))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := bucketMap[bucket]; !ok {
|
||||||
|
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(BucketExists); !ok {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucketMap[bucket] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if globalSiteReplicationSys.isEnabled() && v.Suspended() {
|
||||||
|
writeErrorResponse(ctx, w, APIError{
|
||||||
|
Code: "InvalidBucketState",
|
||||||
|
Description: "Cluster replication is enabled for this site, so the versioning state cannot be changed.",
|
||||||
|
HTTPStatusCode: http.StatusConflict,
|
||||||
|
}, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
|
||||||
|
writeErrorResponse(ctx, w, APIError{
|
||||||
|
Code: "InvalidBucketState",
|
||||||
|
Description: "An Object Lock configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||||
|
HTTPStatusCode: http.StatusConflict,
|
||||||
|
}, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
|
||||||
|
writeErrorResponse(ctx, w, APIError{
|
||||||
|
Code: "InvalidBucketState",
|
||||||
|
Description: "A replication configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||||
|
HTTPStatusCode: http.StatusConflict,
|
||||||
|
}, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketVersioningConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range zr.File {
|
||||||
|
reader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, ""), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sz := file.FileInfo().Size()
|
||||||
|
slc := strings.Split(file.Name, slashSeparator)
|
||||||
|
if len(slc) != 2 { // expecting bucket/configfile in the zipfile
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b, fileName := slc[0], slc[1]
|
||||||
|
if bucket == "" { // use bucket requested in query parameters if specified. Otherwise default bucket name to directory name within zip
|
||||||
|
bucket = b
|
||||||
|
}
|
||||||
|
// create bucket if it does not exist yet.
|
||||||
|
if _, ok := bucketMap[bucket]; !ok {
|
||||||
|
err = objectAPI.MakeBucketWithLocation(ctx, bucket, BucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(BucketExists); !ok {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucketMap[bucket] = struct{}{}
|
||||||
|
}
|
||||||
|
switch fileName {
|
||||||
|
case bucketNotificationConfig:
|
||||||
|
config, err := event.ParseConfig(io.LimitReader(reader, sz), globalSite.Region, globalNotificationSys.targetList)
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
|
if event.IsEventError(err) {
|
||||||
|
apiErr = importError(ctx, err, file.Name, bucket)
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketNotificationConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rulesMap := config.ToRulesMap()
|
||||||
|
globalNotificationSys.AddRulesMap(bucket, rulesMap)
|
||||||
|
case bucketPolicyConfig:
|
||||||
|
// Error out if Content-Length is beyond allowed size.
|
||||||
|
if sz > maxBucketPolicySize {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPolicyTooLarge), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketPolicyBytes, err := ioutil.ReadAll(io.LimitReader(reader, sz))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version in policy must not be empty
|
||||||
|
if bucketPolicy.Version == "" {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedPolicy), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := json.Marshal(bucketPolicy)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Call site replication hook.
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypePolicy,
|
||||||
|
Bucket: bucket,
|
||||||
|
Policy: bucketPolicyBytes,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case bucketLifecycleConfig:
|
||||||
|
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(reader, sz))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the received bucket policy document
|
||||||
|
if err = bucketLifecycle.Validate(); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the transition storage ARNs
|
||||||
|
if err = validateTransitionTier(bucketLifecycle); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(bucketLifecycle)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketSSEConfig:
|
||||||
|
// Parse bucket encryption xml
|
||||||
|
encConfig, err := validateBucketSSEConfig(io.LimitReader(reader, maxBucketSSEConfigSize))
|
||||||
|
if err != nil {
|
||||||
|
apiErr := APIError{
|
||||||
|
Code: "MalformedXML",
|
||||||
|
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, err),
|
||||||
|
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return error if KMS is not initialized
|
||||||
|
if GlobalKMS == nil {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kmsKey := encConfig.KeyID()
|
||||||
|
if kmsKey != "" {
|
||||||
|
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||||
|
_, err := GlobalKMS.GenerateKey(kmsKey, kmsContext)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, errKMSKeyNotFound, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(encConfig)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the bucket encryption configuration in the object layer
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
SSEConfig: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case bucketTaggingConfig:
|
||||||
|
tags, err := tags.ParseBucketXML(io.LimitReader(reader, sz))
|
||||||
|
if err != nil {
|
||||||
|
apiErr := errorCodes.ToAPIErrWithErr(ErrMalformedXML, fmt.Errorf("error importing %s with %w", file.Name, err))
|
||||||
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configData, err := xml.Marshal(tags)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeTags,
|
||||||
|
Bucket: bucket,
|
||||||
|
Tags: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case bucketQuotaConfigFile:
|
||||||
|
data, err := ioutil.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
quotaConfig, err := parseBucketQuota(bucket, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if quotaConfig.Type == "fifo" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketQuotaConfigFile, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketMeta := madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeQuotaConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
Quota: data,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}
|
||||||
|
if quotaConfig.Quota == 0 {
|
||||||
|
bucketMeta.Quota = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, bucketMeta); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, importError(ctx, err, file.Name, bucket), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/minio/kes"
|
"github.com/minio/kes"
|
||||||
@@ -29,7 +30,7 @@ import (
|
|||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, action iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
|
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...iampolicy.AdminAction) (ObjectLayer, auth.Credentials) {
|
||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI := newObjectLayerFn()
|
||||||
if objectAPI == nil || globalNotificationSys == nil {
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
@@ -37,14 +38,17 @@ func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return nil, auth.Credentials{}
|
return nil, auth.Credentials{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate request signature.
|
for _, action := range actions {
|
||||||
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
|
// Validate request signature.
|
||||||
if adminAPIErr != ErrNone {
|
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
if adminAPIErr != ErrNone {
|
||||||
return nil, cred
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||||
|
return nil, cred
|
||||||
|
}
|
||||||
|
return objectAPI, cred
|
||||||
}
|
}
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
return objectAPI, cred
|
return nil, auth.Credentials{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdminError - is a generic error for all admin APIs.
|
// AdminError - is a generic error for all admin APIs.
|
||||||
@@ -83,8 +87,34 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
Description: e.Message,
|
Description: e.Message,
|
||||||
HTTPStatusCode: e.StatusCode,
|
HTTPStatusCode: e.StatusCode,
|
||||||
}
|
}
|
||||||
|
case SRError:
|
||||||
|
apiErr = errorCodes.ToAPIErrWithErr(e.Code, e.Cause)
|
||||||
|
case decomError:
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioDecommissionNotAllowed",
|
||||||
|
Description: e.Err,
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
switch {
|
switch {
|
||||||
|
case errors.Is(err, errTooManyPolicies):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioAdminInvalidRequest",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
case errors.Is(err, errDecommissionAlreadyRunning):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioDecommissionNotAllowed",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
case errors.Is(err, errDecommissionComplete):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioDecommissionNotAllowed",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case errors.Is(err, errConfigNotFound):
|
case errors.Is(err, errConfigNotFound):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioConfigError",
|
Code: "XMinioConfigError",
|
||||||
@@ -97,12 +127,30 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
}
|
}
|
||||||
|
case errors.Is(err, errIAMServiceAccount):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioIAMServiceAccount",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
case errors.Is(err, errIAMServiceAccountUsed):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioIAMServiceAccountUsed",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case errors.Is(err, errIAMNotInitialized):
|
case errors.Is(err, errIAMNotInitialized):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioIAMNotInitialized",
|
Code: "XMinioIAMNotInitialized",
|
||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
}
|
}
|
||||||
|
case errors.Is(err, errPolicyInUse):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioAdminPolicyInUse",
|
||||||
|
Description: "The policy cannot be removed, as it is in use",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
case errors.Is(err, kes.ErrKeyExists):
|
case errors.Is(err, kes.ErrKeyExists):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioKMSKeyExists",
|
Code: "XMinioKMSKeyExists",
|
||||||
@@ -139,7 +187,13 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
|||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinioAdminTierBackendInUse",
|
Code: "XMinioAdminTierBackendInUse",
|
||||||
Description: err.Error(),
|
Description: err.Error(),
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
case errors.Is(err, errTierBackendNotEmpty):
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "XMinioAdminTierBackendNotEmpty",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
case errors.Is(err, errTierInsufficientCreds):
|
case errors.Is(err, errTierInsufficientCreds):
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
@@ -170,3 +224,27 @@ func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
|
|||||||
return toAPIErrorCode(ctx, err)
|
return toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wraps export error for more context
|
||||||
|
func exportError(ctx context.Context, err error, fname, entity string) APIError {
|
||||||
|
if entity == "" {
|
||||||
|
return toAPIError(ctx, fmt.Errorf("error exporting %s with: %w", fname, err))
|
||||||
|
}
|
||||||
|
return toAPIError(ctx, fmt.Errorf("error exporting %s from %s with: %w", entity, fname, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// wraps import error for more context
|
||||||
|
func importError(ctx context.Context, err error, fname, entity string) APIError {
|
||||||
|
if entity == "" {
|
||||||
|
return toAPIError(ctx, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||||
|
}
|
||||||
|
return toAPIError(ctx, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// wraps import error for more context
|
||||||
|
func importErrorWithAPIErr(ctx context.Context, apiErr APIErrorCode, err error, fname, entity string) APIError {
|
||||||
|
if entity == "" {
|
||||||
|
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||||
|
}
|
||||||
|
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -32,7 +33,8 @@ import (
|
|||||||
"github.com/minio/minio/internal/config/etcd"
|
"github.com/minio/minio/internal/config/etcd"
|
||||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||||
"github.com/minio/minio/internal/config/identity/openid"
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
"github.com/minio/minio/internal/config/policy/opa"
|
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||||
|
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||||
"github.com/minio/minio/internal/config/storageclass"
|
"github.com/minio/minio/internal/config/storageclass"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
@@ -63,6 +65,12 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subSys, _, _, err := config.GetSubSys(string(kvBytes))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfg, err := readServerConfig(ctx, objectAPI)
|
cfg, err := readServerConfig(ctx, objectAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -73,11 +81,33 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err = validateConfig(cfg, subSys); err != nil {
|
||||||
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
||||||
|
if dynamic {
|
||||||
|
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
|
||||||
|
r *http.Request, w http.ResponseWriter,
|
||||||
|
) {
|
||||||
|
// Apply dynamic values.
|
||||||
|
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
globalNotificationSys.SignalConfigReload(subSys)
|
||||||
|
// Tell the client that dynamic config was applied.
|
||||||
|
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
||||||
@@ -117,7 +147,13 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateConfig(cfg, objectAPI.SetDriveCounts()); err != nil {
|
subSys, _, _, err := config.GetSubSys(string(kvBytes))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = validateConfig(cfg, subSys); err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -135,14 +171,7 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dynamic {
|
if dynamic {
|
||||||
// Apply dynamic values.
|
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||||
if err := applyDynamicConfig(GlobalContext, objectAPI, cfg); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
globalNotificationSys.SignalService(serviceReloadDynamic)
|
|
||||||
// If all values were dynamic, tell the client.
|
|
||||||
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
|
||||||
}
|
}
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
@@ -160,7 +189,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
cfg := globalServerConfig.Clone()
|
cfg := globalServerConfig.Clone()
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
var buf = &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
cw := config.NewConfigWriteTo(cfg, vars["key"])
|
cw := config.NewConfigWriteTo(cfg, vars["key"])
|
||||||
if _, err := cw.WriteTo(buf); err != nil {
|
if _, err := cw.WriteTo(buf); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -205,11 +234,9 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if err := delServerConfigHistory(ctx, objectAPI, restoreID); err != nil {
|
||||||
if err := delServerConfigHistory(ctx, objectAPI, restoreID); err != nil {
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +275,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateConfig(cfg, objectAPI.SetDriveCounts()); err != nil {
|
if err = validateConfig(cfg, ""); err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -326,7 +353,6 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(rd)
|
json.NewEncoder(w).Encode(rd)
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigHandler - PUT /minio/admin/v3/config
|
// SetConfigHandler - PUT /minio/admin/v3/config
|
||||||
@@ -360,7 +386,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateConfig(cfg, objectAPI.SetDriveCounts()); err != nil {
|
if err = validateConfig(cfg, ""); err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -411,12 +437,16 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
off = !cache.Enabled(kv)
|
off = !cache.Enabled(kv)
|
||||||
case config.StorageClassSubSys:
|
case config.StorageClassSubSys:
|
||||||
off = !storageclass.Enabled(kv)
|
off = !storageclass.Enabled(kv)
|
||||||
case config.PolicyOPASubSys:
|
case config.PolicyPluginSubSys:
|
||||||
off = !opa.Enabled(kv)
|
off = !polplugin.Enabled(kv)
|
||||||
case config.IdentityOpenIDSubSys:
|
case config.IdentityOpenIDSubSys:
|
||||||
off = !openid.Enabled(kv)
|
off = !openid.Enabled(kv)
|
||||||
case config.IdentityLDAPSubSys:
|
case config.IdentityLDAPSubSys:
|
||||||
off = !xldap.Enabled(kv)
|
off = !xldap.Enabled(kv)
|
||||||
|
case config.IdentityTLSSubSys:
|
||||||
|
off = !globalSTSTLSConfig.Enabled
|
||||||
|
case config.IdentityPluginSubSys:
|
||||||
|
off = !idplugin.Enabled(kv)
|
||||||
}
|
}
|
||||||
if off {
|
if off {
|
||||||
s.WriteString(config.KvComment)
|
s.WriteString(config.KvComment)
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
|
"github.com/minio/minio/internal/config"
|
||||||
|
"github.com/minio/minio/internal/config/identity/openid"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List of implemented ID config types.
|
||||||
|
var idCfgTypes = set.CreateStringSet("openid")
|
||||||
|
|
||||||
|
// SetIdentityProviderCfg:
|
||||||
|
//
|
||||||
|
// PUT <admin-prefix>/id-cfg?type=openid&name=dex1
|
||||||
|
func (a adminAPIHandlers) SetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SetIdentityCfg")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||||
|
// More than maxConfigSize bytes were available
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password := cred.SecretKey
|
||||||
|
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgType := mux.Vars(r)["type"]
|
||||||
|
if !idCfgTypes.Contains(cfgType) {
|
||||||
|
// TODO: change this to invalid type error when implementation
|
||||||
|
// is complete.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfgDataBuilder strings.Builder
|
||||||
|
switch cfgType {
|
||||||
|
case "openid":
|
||||||
|
fmt.Fprintf(&cfgDataBuilder, "identity_openid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure body content type is opaque.
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
if contentType != "application/octet-stream" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subsystem configuration name could be empty.
|
||||||
|
cfgName := mux.Vars(r)["name"]
|
||||||
|
if cfgName != "" {
|
||||||
|
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.SubSystemSeparator, cfgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&cfgDataBuilder, "%s%s", config.KvSpaceSeparator, string(reqBytes))
|
||||||
|
|
||||||
|
cfgData := cfgDataBuilder.String()
|
||||||
|
subSys, _, _, err := config.GetSubSys(cfgData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := readServerConfig(ctx, objectAPI)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic, err := cfg.ReadConfig(strings.NewReader(cfgData))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDP config is not dynamic. Sanity check.
|
||||||
|
if dynamic {
|
||||||
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = validateConfig(cfg, subSys); err != nil {
|
||||||
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the actual server config on disk.
|
||||||
|
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to the config input KV to history.
|
||||||
|
if err = saveServerConfigHistory(ctx, objectAPI, []byte(cfgData)); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseHeadersOnly(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentityProviderCfg:
|
||||||
|
//
|
||||||
|
// GET <admin-prefix>/id-cfg?type=openid&name=dex_test
|
||||||
|
//
|
||||||
|
// GetIdentityProviderCfg returns a list of configured IDPs on the server if
|
||||||
|
// name is empty. If name is non-empty, returns the configuration details for
|
||||||
|
// the IDP of the given type and configuration name. The configuration name for
|
||||||
|
// the default ("un-named") configuration target is `_`.
|
||||||
|
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "GetIdentityProviderCfg")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgType := mux.Vars(r)["type"]
|
||||||
|
cfgName := r.Form.Get("name")
|
||||||
|
password := cred.SecretKey
|
||||||
|
|
||||||
|
if !idCfgTypes.Contains(cfgType) {
|
||||||
|
// TODO: change this to invalid type error when implementation
|
||||||
|
// is complete.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no cfgName is provided, we list.
|
||||||
|
if cfgName == "" {
|
||||||
|
a.listIdentityProviders(ctx, w, r, cfgType, password)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := globalServerConfig.Clone()
|
||||||
|
|
||||||
|
cfgInfos, err := globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||||
|
if err != nil {
|
||||||
|
if err == openid.ErrProviderConfigNotFound {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := madmin.IDPConfig{
|
||||||
|
Type: cfgType,
|
||||||
|
Name: cfgName,
|
||||||
|
Info: cfgInfos,
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(res)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
econfigData, err := madmin.EncryptData(password, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) listIdentityProviders(ctx context.Context, w http.ResponseWriter, r *http.Request, cfgType, password string) {
|
||||||
|
// var subSys string
|
||||||
|
switch cfgType {
|
||||||
|
case "openid":
|
||||||
|
// subSys = config.IdentityOpenIDSubSys
|
||||||
|
default:
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := globalServerConfig.Clone()
|
||||||
|
cfgList, err := globalOpenIDConfig.GetConfigList(cfg)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cfgList)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
econfigData, err := madmin.EncryptData(password, data)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, econfigData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteIdentityProviderCfg:
|
||||||
|
//
|
||||||
|
// DELETE <admin-prefix>/id-cfg?type=openid&name=dex_test
|
||||||
|
func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "DeleteIdentityProviderCfg")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConfigUpdateAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgType := mux.Vars(r)["type"]
|
||||||
|
cfgName := mux.Vars(r)["name"]
|
||||||
|
if !idCfgTypes.Contains(cfgType) {
|
||||||
|
// TODO: change this to invalid type error when implementation
|
||||||
|
// is complete.
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := globalServerConfig.Clone()
|
||||||
|
|
||||||
|
cfgInfos, err := globalOpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||||
|
if err != nil {
|
||||||
|
if err == openid.ErrProviderConfigNotFound {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEnv := false
|
||||||
|
for _, ci := range cfgInfos {
|
||||||
|
if ci.IsCfg && ci.IsEnv {
|
||||||
|
hasEnv = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasEnv {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var subSys string
|
||||||
|
switch cfgType {
|
||||||
|
case "openid":
|
||||||
|
subSys = config.IdentityOpenIDSubSys
|
||||||
|
default:
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err = readServerConfig(ctx, objectAPI)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cfg.DelKVS(fmt.Sprintf("%s:%s", subSys, cfgName)); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = validateConfig(cfg, subSys); err != nil {
|
||||||
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
||||||
|
if dynamic {
|
||||||
|
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "StartDecommission")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DecommissionAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||||
|
if globalEndpoints.Legacy() {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pools, ok := objectAPI.(*erasureServerPools)
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
v := vars["pool"]
|
||||||
|
|
||||||
|
idx := globalEndpoints.GetPoolIdx(v)
|
||||||
|
if idx == -1 {
|
||||||
|
// We didn't find any matching pools, invalid input
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep := globalEndpoints[idx].Endpoints[0]; !ep.IsLocal {
|
||||||
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
|
if proxyEp.Endpoint.Host == ep.Host {
|
||||||
|
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pools.Decommission(r.Context(), idx); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "CancelDecommission")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DecommissionAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||||
|
if globalEndpoints.Legacy() {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pools, ok := objectAPI.(*erasureServerPools)
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
v := vars["pool"]
|
||||||
|
|
||||||
|
idx := globalEndpoints.GetPoolIdx(v)
|
||||||
|
if idx == -1 {
|
||||||
|
// We didn't find any matching pools, invalid input
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ep := globalEndpoints[idx].Endpoints[0]; !ep.IsLocal {
|
||||||
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
|
if proxyEp.Endpoint.Host == ep.Host {
|
||||||
|
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pools.DecommissionCancel(ctx, idx); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "StatusPool")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction, iampolicy.DecommissionAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||||
|
if globalEndpoints.Legacy() {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pools, ok := objectAPI.(*erasureServerPools)
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
v := vars["pool"]
|
||||||
|
|
||||||
|
idx := globalEndpoints.GetPoolIdx(v)
|
||||||
|
if idx == -1 {
|
||||||
|
apiErr := toAdminAPIErr(ctx, errInvalidArgument)
|
||||||
|
apiErr.Description = fmt.Sprintf("specified pool '%s' not found, please specify a valid pool", v)
|
||||||
|
// We didn't find any matching pools, invalid input
|
||||||
|
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := pools.Status(r.Context(), idx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogIf(r.Context(), json.NewEncoder(w).Encode(&status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "ListPools")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction, iampolicy.DecommissionAdminAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||||
|
if globalEndpoints.Legacy() {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pools, ok := objectAPI.(*erasureServerPools)
|
||||||
|
if !ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
poolsStatus := make([]PoolStatus, len(globalEndpoints))
|
||||||
|
for idx := range globalEndpoints {
|
||||||
|
status, err := pools.Status(r.Context(), idx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
poolsStatus[idx] = status
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogIf(r.Context(), json.NewEncoder(w).Encode(poolsStatus))
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
|
"github.com/minio/pkg/bucket/policy"
|
||||||
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SiteReplicationAdd - PUT /minio/admin/v3/site-replication/add
|
||||||
|
func (a adminAPIHandlers) SiteReplicationAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationAdd")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationAddAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sites []madmin.PeerSite
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &sites, cred.SecretKey); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := globalSiteReplicationSys.AddPeerClusters(ctx, sites)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerJoin - PUT /minio/admin/v3/site-replication/join
|
||||||
|
//
|
||||||
|
// used internally to tell current cluster to enable SR with
|
||||||
|
// the provided peer clusters and service account.
|
||||||
|
func (a adminAPIHandlers) SRPeerJoin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerJoin")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationAddAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var joinArg madmin.SRPeerJoinReq
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &joinArg, cred.SecretKey); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalSiteReplicationSys.PeerJoinReq(ctx, joinArg); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerBucketOps - PUT /minio/admin/v3/site-replication/bucket-ops?bucket=x&operation=y
|
||||||
|
func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerBucketOps")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationOperationAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
bucket := vars["bucket"]
|
||||||
|
operation := madmin.BktOp(vars["operation"])
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch operation {
|
||||||
|
default:
|
||||||
|
err = errSRInvalidRequest(errInvalidArgument)
|
||||||
|
case madmin.MakeWithVersioningBktOp:
|
||||||
|
_, isLockEnabled := r.Form["lockEnabled"]
|
||||||
|
_, isVersioningEnabled := r.Form["versioningEnabled"]
|
||||||
|
_, isForceCreate := r.Form["forceCreate"]
|
||||||
|
opts := BucketOptions{
|
||||||
|
Location: r.Form.Get("location"),
|
||||||
|
LockEnabled: isLockEnabled,
|
||||||
|
VersioningEnabled: isVersioningEnabled,
|
||||||
|
ForceCreate: isForceCreate,
|
||||||
|
}
|
||||||
|
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||||
|
case madmin.ConfigureReplBktOp:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||||
|
case madmin.DeleteBucketBktOp:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, false)
|
||||||
|
case madmin.ForceDeleteBucketBktOp:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, true)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerReplicateIAMItem - PUT /minio/admin/v3/site-replication/iam-item
|
||||||
|
func (a adminAPIHandlers) SRPeerReplicateIAMItem(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerReplicateIAMItem")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationOperationAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var item madmin.SRIAMItem
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch item.Type {
|
||||||
|
default:
|
||||||
|
err = errSRInvalidRequest(errInvalidArgument)
|
||||||
|
case madmin.SRIAMItemPolicy:
|
||||||
|
if item.Policy == nil {
|
||||||
|
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
|
||||||
|
} else {
|
||||||
|
policy, perr := iampolicy.ParseConfig(bytes.NewReader(item.Policy))
|
||||||
|
if perr != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, perr), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if policy.IsEmpty() {
|
||||||
|
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
|
||||||
|
} else {
|
||||||
|
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, policy, item.UpdatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case madmin.SRIAMItemSvcAcc:
|
||||||
|
err = globalSiteReplicationSys.PeerSvcAccChangeHandler(ctx, item.SvcAccChange, item.UpdatedAt)
|
||||||
|
case madmin.SRIAMItemPolicyMapping:
|
||||||
|
err = globalSiteReplicationSys.PeerPolicyMappingHandler(ctx, item.PolicyMapping, item.UpdatedAt)
|
||||||
|
case madmin.SRIAMItemSTSAcc:
|
||||||
|
err = globalSiteReplicationSys.PeerSTSAccHandler(ctx, item.STSCredential, item.UpdatedAt)
|
||||||
|
case madmin.SRIAMItemIAMUser:
|
||||||
|
err = globalSiteReplicationSys.PeerIAMUserChangeHandler(ctx, item.IAMUser, item.UpdatedAt)
|
||||||
|
case madmin.SRIAMItemGroupInfo:
|
||||||
|
err = globalSiteReplicationSys.PeerGroupInfoChangeHandler(ctx, item.GroupInfo, item.UpdatedAt)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerReplicateBucketItem - PUT /minio/admin/v3/site-replication/bucket-meta
|
||||||
|
func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerReplicateBucketItem")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationOperationAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var item madmin.SRBucketMeta
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch item.Type {
|
||||||
|
default:
|
||||||
|
err = errSRInvalidRequest(errInvalidArgument)
|
||||||
|
case madmin.SRBucketMetaTypePolicy:
|
||||||
|
if item.Policy == nil {
|
||||||
|
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||||
|
} else {
|
||||||
|
bktPolicy, berr := policy.ParseConfig(bytes.NewReader(item.Policy), item.Bucket)
|
||||||
|
if berr != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, berr), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if bktPolicy.IsEmpty() {
|
||||||
|
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||||
|
} else {
|
||||||
|
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy, item.UpdatedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case madmin.SRBucketMetaTypeQuotaConfig:
|
||||||
|
if item.Quota == nil {
|
||||||
|
err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||||
|
} else {
|
||||||
|
quotaConfig, err := parseBucketQuota(item.Bucket, item.Quota)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, quotaConfig, item.UpdatedAt); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case madmin.SRBucketMetaTypeVersionConfig:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning, item.UpdatedAt)
|
||||||
|
case madmin.SRBucketMetaTypeTags:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
|
||||||
|
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketObjectLockConfigHandler(ctx, item.Bucket, item.ObjectLockConfig, item.UpdatedAt)
|
||||||
|
case madmin.SRBucketMetaTypeSSEConfig:
|
||||||
|
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteReplicationInfo - GET /minio/admin/v3/site-replication/info
|
||||||
|
func (a adminAPIHandlers) SiteReplicationInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationInfoAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := globalSiteReplicationSys.GetClusterInfo(ctx)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a adminAPIHandlers) SRPeerGetIDPSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationGetIDPSettings")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationAddAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idpSettings := globalSiteReplicationSys.GetIDPSettings(ctx)
|
||||||
|
if err := json.NewEncoder(w).Encode(idpSettings); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseJSONBody(ctx context.Context, body io.Reader, v interface{}, encryptionKey string) error {
|
||||||
|
data, err := ioutil.ReadAll(body)
|
||||||
|
if err != nil {
|
||||||
|
return SRError{
|
||||||
|
Cause: err,
|
||||||
|
Code: ErrSiteReplicationInvalidRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if encryptionKey != "" {
|
||||||
|
data, err = madmin.DecryptData(encryptionKey, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return SRError{
|
||||||
|
Cause: err,
|
||||||
|
Code: ErrSiteReplicationInvalidRequest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.Unmarshal(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteReplicationStatus - GET /minio/admin/v3/site-replication/status
|
||||||
|
func (a adminAPIHandlers) SiteReplicationStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationStatus")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationInfoAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts := getSRStatusOptions(r)
|
||||||
|
// default options to all if status options are unset for backward compatibility
|
||||||
|
var dfltOpts madmin.SRStatusOptions
|
||||||
|
if opts == dfltOpts {
|
||||||
|
opts.Buckets = true
|
||||||
|
opts.Users = true
|
||||||
|
opts.Policies = true
|
||||||
|
opts.Groups = true
|
||||||
|
}
|
||||||
|
info, err := globalSiteReplicationSys.SiteReplicationStatus(ctx, objectAPI, opts)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteReplicationMetaInfo - GET /minio/admin/v3/site-replication/metainfo
|
||||||
|
func (a adminAPIHandlers) SiteReplicationMetaInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationMetaInfo")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationInfoAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := getSRStatusOptions(r)
|
||||||
|
info, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, objectAPI, opts)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteReplicationEdit - PUT /minio/admin/v3/site-replication/edit
|
||||||
|
func (a adminAPIHandlers) SiteReplicationEdit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationEdit")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationAddAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var site madmin.PeerInfo
|
||||||
|
err := parseJSONBody(ctx, r.Body, &site, cred.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := globalSiteReplicationSys.EditPeerCluster(ctx, site)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerEdit - PUT /minio/admin/v3/site-replication/peer/edit
|
||||||
|
//
|
||||||
|
// used internally to tell current cluster to update endpoint for peer
|
||||||
|
func (a adminAPIHandlers) SRPeerEdit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerEdit")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationAddAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pi madmin.PeerInfo
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &pi, ""); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalSiteReplicationSys.PeerEditReq(ctx, pi); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSRStatusOptions(r *http.Request) (opts madmin.SRStatusOptions) {
|
||||||
|
q := r.Form
|
||||||
|
opts.Buckets = q.Get("buckets") == "true"
|
||||||
|
opts.Policies = q.Get("policies") == "true"
|
||||||
|
opts.Groups = q.Get("groups") == "true"
|
||||||
|
opts.Users = q.Get("users") == "true"
|
||||||
|
opts.Entity = madmin.GetSREntityType(q.Get("entity"))
|
||||||
|
opts.EntityValue = q.Get("entityvalue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SiteReplicationRemove - PUT /minio/admin/v3/site-replication/remove
|
||||||
|
func (a adminAPIHandlers) SiteReplicationRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SiteReplicationRemove")
|
||||||
|
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationRemoveAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rreq madmin.SRRemoveReq
|
||||||
|
err := parseJSONBody(ctx, r.Body, &rreq, "")
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := globalSiteReplicationSys.RemovePeerCluster(ctx, objectAPI, rreq)
|
||||||
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeSuccessResponseJSON(w, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SRPeerRemove - PUT /minio/admin/v3/site-replication/peer/remove
|
||||||
|
//
|
||||||
|
// used internally to tell current cluster to update endpoint for peer
|
||||||
|
func (a adminAPIHandlers) SRPeerRemove(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := newContext(r, w, "SRPeerRemove")
|
||||||
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SiteReplicationRemoveAction)
|
||||||
|
if objectAPI == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req madmin.SRRemoveReq
|
||||||
|
if err := parseJSONBody(ctx, r.Body, &req, ""); err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalSiteReplicationSys.InternalRemoveReq(ctx, objectAPI, req); err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//go:build !race
|
||||||
|
// +build !race
|
||||||
|
|
||||||
|
// Tests in this file are not run under the `-race` flag as they are too slow
|
||||||
|
// and cause context deadline errors.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
minio "github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
||||||
|
suite.SetUpSuite(c)
|
||||||
|
suite.TestDeleteUserRace(c)
|
||||||
|
suite.TearDownSuite(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
||||||
|
if runtime.GOOS == globalWindowsOSName {
|
||||||
|
t.Skip("windows is clunky")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseTestCases := []TestSuiteCommon{
|
||||||
|
// Init and run test on ErasureSD backend with signature v4.
|
||||||
|
{serverType: "ErasureSD", signer: signerV4},
|
||||||
|
// Init and run test on ErasureSD backend, with tls enabled.
|
||||||
|
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||||
|
// Init and run test on Erasure backend.
|
||||||
|
{serverType: "Erasure", signer: signerV4},
|
||||||
|
// Init and run test on ErasureSet backend.
|
||||||
|
{serverType: "ErasureSet", signer: signerV4},
|
||||||
|
}
|
||||||
|
testCases := []*TestSuiteIAM{}
|
||||||
|
for _, bt := range baseTestCases {
|
||||||
|
testCases = append(testCases,
|
||||||
|
newTestSuiteIAM(bt, false),
|
||||||
|
newTestSuiteIAM(bt, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
etcdStr := ""
|
||||||
|
if testCase.withEtcdBackend {
|
||||||
|
etcdStr = " (with etcd backend)"
|
||||||
|
}
|
||||||
|
t.Run(
|
||||||
|
fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
|
||||||
|
func(t *testing.T) {
|
||||||
|
runAllIAMConcurrencyTests(testCase, &check{t, testCase.serverType})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
bucket := getRandomBucketName()
|
||||||
|
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("bucket creat error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a policy policy
|
||||||
|
policy := "mypolicy"
|
||||||
|
policyBytes := []byte(fmt.Sprintf(`{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::%s/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, bucket))
|
||||||
|
err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("policy add error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userCount := 50
|
||||||
|
accessKeys := make([]string, userCount)
|
||||||
|
secretKeys := make([]string, userCount)
|
||||||
|
for i := 0; i < userCount; i++ {
|
||||||
|
accessKey, secretKey := mustGenerateCredentials(c)
|
||||||
|
err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.adm.SetPolicy(ctx, policy, accessKey, false)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("Unable to set policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessKeys[i] = accessKey
|
||||||
|
secretKeys[i] = secretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
for i := 0; i < userCount; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
||||||
|
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
||||||
|
if err != nil {
|
||||||
|
c.Fatalf("unable to remove user: %v", err)
|
||||||
|
}
|
||||||
|
c.mustNotListObjects(ctx, uClient, bucket)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
+1113
-237
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+794
-189
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
@@ -40,11 +41,13 @@ type adminErasureTestBed struct {
|
|||||||
erasureDirs []string
|
erasureDirs []string
|
||||||
objLayer ObjectLayer
|
objLayer ObjectLayer
|
||||||
router *mux.Router
|
router *mux.Router
|
||||||
|
done context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareAdminErasureTestBed - helper function that setups a single-node
|
// prepareAdminErasureTestBed - helper function that setups a single-node
|
||||||
// Erasure backend for admin-handler tests.
|
// Erasure backend for admin-handler tests.
|
||||||
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
// reset global variables to start afresh.
|
// reset global variables to start afresh.
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
@@ -56,11 +59,13 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
|||||||
// Initializing objectLayer for HealFormatHandler.
|
// Initializing objectLayer for HealFormatHandler.
|
||||||
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
||||||
if xlErr != nil {
|
if xlErr != nil {
|
||||||
|
cancel()
|
||||||
return nil, xlErr
|
return nil, xlErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize minio server config.
|
// Initialize minio server config.
|
||||||
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||||
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,11 +74,11 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
|||||||
|
|
||||||
globalEndpoints = mustGetPoolEndpoints(erasureDirs...)
|
globalEndpoints = mustGetPoolEndpoints(erasureDirs...)
|
||||||
|
|
||||||
newAllSubsystems()
|
initAllSubsystems()
|
||||||
|
|
||||||
initAllSubsystems(ctx, objLayer)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
globalIAMSys.InitStore(objLayer)
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
// Setup admin mgmt REST API handlers.
|
// Setup admin mgmt REST API handlers.
|
||||||
adminRouter := mux.NewRouter()
|
adminRouter := mux.NewRouter()
|
||||||
@@ -83,12 +88,14 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
|||||||
erasureDirs: erasureDirs,
|
erasureDirs: erasureDirs,
|
||||||
objLayer: objLayer,
|
objLayer: objLayer,
|
||||||
router: adminRouter,
|
router: adminRouter,
|
||||||
|
done: cancel,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TearDown - method that resets the test bed for subsequent unit
|
// TearDown - method that resets the test bed for subsequent unit
|
||||||
// tests to start afresh.
|
// tests to start afresh.
|
||||||
func (atb *adminErasureTestBed) TearDown() {
|
func (atb *adminErasureTestBed) TearDown() {
|
||||||
|
atb.done()
|
||||||
removeRoots(atb.erasureDirs)
|
removeRoots(atb.erasureDirs)
|
||||||
resetTestGlobals()
|
resetTestGlobals()
|
||||||
}
|
}
|
||||||
@@ -229,8 +236,8 @@ func TestServiceRestartHandler(t *testing.T) {
|
|||||||
|
|
||||||
// buildAdminRequest - helper function to build an admin API request.
|
// buildAdminRequest - helper function to build an admin API request.
|
||||||
func buildAdminRequest(queryVal url.Values, method, path string,
|
func buildAdminRequest(queryVal url.Values, method, path string,
|
||||||
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
|
||||||
|
) {
|
||||||
req, err := newTestRequest(method,
|
req, err := newTestRequest(method,
|
||||||
adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
|
adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
|
||||||
contentLength, bodySeeker)
|
contentLength, bodySeeker)
|
||||||
@@ -373,5 +380,4 @@ func TestExtractHealInitParams(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+56
-131
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -90,8 +91,11 @@ type allHealState struct {
|
|||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
|
||||||
// map of heal path to heal sequence
|
// map of heal path to heal sequence
|
||||||
healSeqMap map[string]*healSequence // Indexed by endpoint
|
healSeqMap map[string]*healSequence // Indexed by endpoint
|
||||||
healLocalDisks map[Endpoint]struct{}
|
// keep track of the healing status of disks in the memory
|
||||||
|
// false: the disk needs to be healed but no healing routine is started
|
||||||
|
// true: the disk is currently healing
|
||||||
|
healLocalDisks map[Endpoint]bool
|
||||||
healStatus map[string]healingTracker // Indexed by disk ID
|
healStatus map[string]healingTracker // Indexed by disk ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +103,7 @@ type allHealState struct {
|
|||||||
func newHealState(cleanup bool) *allHealState {
|
func newHealState(cleanup bool) *allHealState {
|
||||||
hstate := &allHealState{
|
hstate := &allHealState{
|
||||||
healSeqMap: make(map[string]*healSequence),
|
healSeqMap: make(map[string]*healSequence),
|
||||||
healLocalDisks: map[Endpoint]struct{}{},
|
healLocalDisks: make(map[Endpoint]bool),
|
||||||
healStatus: make(map[string]healingTracker),
|
healStatus: make(map[string]healingTracker),
|
||||||
}
|
}
|
||||||
if cleanup {
|
if cleanup {
|
||||||
@@ -108,13 +112,6 @@ func newHealState(cleanup bool) *allHealState {
|
|||||||
return hstate
|
return hstate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ahs *allHealState) healDriveCount() int {
|
|
||||||
ahs.RLock()
|
|
||||||
defer ahs.RUnlock()
|
|
||||||
|
|
||||||
return len(ahs.healLocalDisks)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
|
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||||
ahs.Lock()
|
ahs.Lock()
|
||||||
defer ahs.Unlock()
|
defer ahs.Unlock()
|
||||||
@@ -164,23 +161,34 @@ func (ahs *allHealState) getLocalHealingDisks() map[string]madmin.HealingDisk {
|
|||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHealLocalDiskEndpoints() returns the list of disks that need
|
||||||
|
// to be healed but there is no healing routine in progress on them.
|
||||||
func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
|
func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
|
||||||
ahs.RLock()
|
ahs.RLock()
|
||||||
defer ahs.RUnlock()
|
defer ahs.RUnlock()
|
||||||
|
|
||||||
var endpoints Endpoints
|
var endpoints Endpoints
|
||||||
for ep := range ahs.healLocalDisks {
|
for ep, healing := range ahs.healLocalDisks {
|
||||||
endpoints = append(endpoints, ep)
|
if !healing {
|
||||||
|
endpoints = append(endpoints, ep)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return endpoints
|
return endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ahs *allHealState) markDiskForHealing(ep Endpoint) {
|
||||||
|
ahs.Lock()
|
||||||
|
defer ahs.Unlock()
|
||||||
|
|
||||||
|
ahs.healLocalDisks[ep] = true
|
||||||
|
}
|
||||||
|
|
||||||
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
|
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||||
ahs.Lock()
|
ahs.Lock()
|
||||||
defer ahs.Unlock()
|
defer ahs.Unlock()
|
||||||
|
|
||||||
for _, ep := range healLocalDisks {
|
for _, ep := range healLocalDisks {
|
||||||
ahs.healLocalDisks[ep] = struct{}{}
|
ahs.healLocalDisks[ep] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +201,6 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-periodicTimer.C:
|
case <-periodicTimer.C:
|
||||||
periodicTimer.Reset(time.Minute * 5)
|
|
||||||
now := UTCNow()
|
now := UTCNow()
|
||||||
ahs.Lock()
|
ahs.Lock()
|
||||||
for path, h := range ahs.healSeqMap {
|
for path, h := range ahs.healSeqMap {
|
||||||
@@ -202,6 +209,8 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ahs.Unlock()
|
ahs.Unlock()
|
||||||
|
|
||||||
|
periodicTimer.Reset(time.Minute * 5)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// server could be restarting - need
|
// server could be restarting - need
|
||||||
// to exit immediately
|
// to exit immediately
|
||||||
@@ -277,8 +286,8 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
|
|||||||
// background routine to clean up heal results after the
|
// background routine to clean up heal results after the
|
||||||
// aforementioned duration.
|
// aforementioned duration.
|
||||||
func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLayer) (
|
func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLayer) (
|
||||||
respBytes []byte, apiErr APIError, errMsg string) {
|
respBytes []byte, apiErr APIError, errMsg string,
|
||||||
|
) {
|
||||||
if h.forceStarted {
|
if h.forceStarted {
|
||||||
_, apiErr = ahs.stopHealSequence(pathJoin(h.bucket, h.object))
|
_, apiErr = ahs.stopHealSequence(pathJoin(h.bucket, h.object))
|
||||||
if apiErr.Code != "" {
|
if apiErr.Code != "" {
|
||||||
@@ -337,8 +346,8 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLay
|
|||||||
// representation. The clientToken helps ensure there aren't
|
// representation. The clientToken helps ensure there aren't
|
||||||
// conflicting clients fetching status.
|
// conflicting clients fetching status.
|
||||||
func (ahs *allHealState) PopHealStatusJSON(hpath string,
|
func (ahs *allHealState) PopHealStatusJSON(hpath string,
|
||||||
clientToken string) ([]byte, APIErrorCode) {
|
clientToken string) ([]byte, APIErrorCode,
|
||||||
|
) {
|
||||||
// fetch heal state for given path
|
// fetch heal state for given path
|
||||||
h, exists := ahs.getHealSequence(hpath)
|
h, exists := ahs.getHealSequence(hpath)
|
||||||
if !exists {
|
if !exists {
|
||||||
@@ -396,9 +405,6 @@ type healSequence struct {
|
|||||||
// bucket, and object on which heal seq. was initiated
|
// bucket, and object on which heal seq. was initiated
|
||||||
bucket, object string
|
bucket, object string
|
||||||
|
|
||||||
// A channel of entities (format, buckets, objects) to heal
|
|
||||||
sourceCh chan healSource
|
|
||||||
|
|
||||||
// A channel of entities with heal result
|
// A channel of entities with heal result
|
||||||
respCh chan healResult
|
respCh chan healResult
|
||||||
|
|
||||||
@@ -455,8 +461,8 @@ type healSequence struct {
|
|||||||
// NewHealSequence - creates healSettings, assumes bucket and
|
// NewHealSequence - creates healSettings, assumes bucket and
|
||||||
// objPrefix are already validated.
|
// objPrefix are already validated.
|
||||||
func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
||||||
hs madmin.HealOpts, forceStart bool) *healSequence {
|
hs madmin.HealOpts, forceStart bool,
|
||||||
|
) *healSequence {
|
||||||
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
||||||
reqInfo.AppendTags("prefix", objPrefix)
|
reqInfo.AppendTags("prefix", objPrefix)
|
||||||
ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
|
ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
|
||||||
@@ -493,7 +499,7 @@ func (h *healSequence) getScannedItemsCount() int64 {
|
|||||||
defer h.mutex.RUnlock()
|
defer h.mutex.RUnlock()
|
||||||
|
|
||||||
for _, v := range h.scannedItemsMap {
|
for _, v := range h.scannedItemsMap {
|
||||||
count = count + v
|
count += v
|
||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
@@ -583,12 +589,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
|||||||
// heal-results in memory and the client has not consumed it
|
// heal-results in memory and the client has not consumed it
|
||||||
// for too long.
|
// for too long.
|
||||||
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
||||||
defer func() {
|
defer unconsumedTimer.Stop()
|
||||||
// stop the timeout timer so it is garbage collected.
|
|
||||||
if !unconsumedTimer.Stop() {
|
|
||||||
<-unconsumedTimer.C
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var itemsLen int
|
var itemsLen int
|
||||||
for {
|
for {
|
||||||
@@ -648,11 +649,7 @@ func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
|
|||||||
h.currentStatus.StartTime = UTCNow()
|
h.currentStatus.StartTime = UTCNow()
|
||||||
h.mutex.Unlock()
|
h.mutex.Unlock()
|
||||||
|
|
||||||
if h.sourceCh == nil {
|
go h.traverseAndHeal(objAPI)
|
||||||
go h.traverseAndHeal(objAPI)
|
|
||||||
} else {
|
|
||||||
go h.healFromSourceCh()
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err, ok := <-h.traverseAndHealDoneCh:
|
case err, ok := <-h.traverseAndHealDoneCh:
|
||||||
@@ -696,45 +693,39 @@ func (h *healSequence) logHeal(healType madmin.HealItemType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
||||||
globalHealConfigMu.Lock()
|
|
||||||
opts := globalHealConfig
|
|
||||||
globalHealConfigMu.Unlock()
|
|
||||||
|
|
||||||
// Send heal request
|
// Send heal request
|
||||||
task := healTask{
|
task := healTask{
|
||||||
bucket: source.bucket,
|
bucket: source.bucket,
|
||||||
object: source.object,
|
object: source.object,
|
||||||
versionID: source.versionID,
|
versionID: source.versionID,
|
||||||
opts: h.settings,
|
opts: h.settings,
|
||||||
responseCh: h.respCh,
|
respCh: h.respCh,
|
||||||
}
|
}
|
||||||
if source.opts != nil {
|
if source.opts != nil {
|
||||||
task.opts = *source.opts
|
task.opts = *source.opts
|
||||||
|
} else {
|
||||||
|
task.opts.ScanMode = madmin.HealNormalScan
|
||||||
}
|
}
|
||||||
if opts.Bitrot {
|
|
||||||
task.opts.ScanMode = madmin.HealDeepScan
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait and proceed if there are active requests
|
|
||||||
waitForLowHTTPReq(opts.IOCount, opts.Sleep)
|
|
||||||
|
|
||||||
h.mutex.Lock()
|
h.mutex.Lock()
|
||||||
h.scannedItemsMap[healType]++
|
h.scannedItemsMap[healType]++
|
||||||
h.lastHealActivity = UTCNow()
|
h.lastHealActivity = UTCNow()
|
||||||
h.mutex.Unlock()
|
h.mutex.Unlock()
|
||||||
|
|
||||||
globalBackgroundHealRoutine.queueHealTask(task)
|
select {
|
||||||
|
case globalBackgroundHealRoutine.tasks <- task:
|
||||||
|
if serverDebugLog {
|
||||||
|
logger.Info("Task in the queue: %#v", task)
|
||||||
|
}
|
||||||
|
case <-h.ctx.Done():
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case res := <-h.respCh:
|
case res := <-h.respCh:
|
||||||
if !h.reportProgress {
|
if !h.reportProgress {
|
||||||
// Object might have been deleted, by the time heal
|
if errors.Is(res.err, errSkipFile) { // this is only sent usually by nopHeal
|
||||||
// was attempted, we should ignore this object and
|
return nil
|
||||||
// return the error and not calculate this object
|
|
||||||
// as part of the metrics.
|
|
||||||
if isErrObjectNotFound(res.err) || isErrVersionNotFound(res.err) {
|
|
||||||
// Return the error so that caller can handle it.
|
|
||||||
return res.err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h.mutex.Lock()
|
h.mutex.Lock()
|
||||||
@@ -759,11 +750,6 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
}
|
}
|
||||||
res.result.Type = healType
|
res.result.Type = healType
|
||||||
if res.err != nil {
|
if res.err != nil {
|
||||||
// Object might have been deleted, by the time heal
|
|
||||||
// was attempted, we should ignore this object and return success.
|
|
||||||
if isErrObjectNotFound(res.err) || isErrVersionNotFound(res.err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Only report object error
|
// Only report object error
|
||||||
if healType != madmin.HealItemObject {
|
if healType != madmin.HealItemObject {
|
||||||
return res.err
|
return res.err
|
||||||
@@ -776,48 +762,6 @@ func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) healItemsFromSourceCh() error {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case source, ok := <-h.sourceCh:
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemType madmin.HealItemType
|
|
||||||
switch source.bucket {
|
|
||||||
case nopHeal:
|
|
||||||
continue
|
|
||||||
case SlashSeparator:
|
|
||||||
itemType = madmin.HealItemMetadata
|
|
||||||
default:
|
|
||||||
if source.object == "" {
|
|
||||||
itemType = madmin.HealItemBucket
|
|
||||||
} else {
|
|
||||||
itemType = madmin.HealItemObject
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.queueHealTask(source, itemType); err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case ObjectExistsAsDirectory:
|
|
||||||
case ObjectNotFound:
|
|
||||||
case VersionNotFound:
|
|
||||||
default:
|
|
||||||
logger.LogIf(h.ctx, fmt.Errorf("Heal attempt failed for %s: %w",
|
|
||||||
pathJoin(source.bucket, source.object), err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-h.ctx.Done():
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *healSequence) healFromSourceCh() {
|
|
||||||
h.healItemsFromSourceCh()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
||||||
// Start healing the config prefix.
|
// Start healing the config prefix.
|
||||||
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
||||||
@@ -862,26 +806,11 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
|||||||
object: object,
|
object: object,
|
||||||
versionID: versionID,
|
versionID: versionID,
|
||||||
}, madmin.HealItemBucketMetadata)
|
}, madmin.HealItemBucketMetadata)
|
||||||
// Object might have been deleted, by the time heal
|
|
||||||
// was attempted we ignore this object an move on.
|
|
||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// healDiskFormat - heals format.json, return value indicates if a
|
|
||||||
// failure error occurred.
|
|
||||||
func (h *healSequence) healDiskFormat() error {
|
|
||||||
if h.isQuitting() {
|
|
||||||
return errHealStopSignalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.queueHealTask(healSource{bucket: SlashSeparator}, madmin.HealItemMetadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// healBuckets - check for all buckets heal or just particular bucket.
|
// healBuckets - check for all buckets heal or just particular bucket.
|
||||||
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
||||||
if h.isQuitting() {
|
if h.isQuitting() {
|
||||||
@@ -915,9 +844,7 @@ func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
|||||||
// healBucket - traverses and heals given bucket
|
// healBucket - traverses and heals given bucket
|
||||||
func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly bool) error {
|
func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly bool) error {
|
||||||
if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil {
|
if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil {
|
||||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucketsOnly {
|
if bucketsOnly {
|
||||||
@@ -931,9 +858,6 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
|
|||||||
oi, err := objAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
|
oi, err := objAPI.GetObjectInfo(h.ctx, bucket, h.object, ObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
|
if err = h.healObject(bucket, h.object, oi.VersionID); err != nil {
|
||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,11 +867,7 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
||||||
// Object might have been deleted, by the time heal
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
// was attempted we ignore this object an move on.
|
|
||||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
|
||||||
return errFnHealFromAPIErr(h.ctx, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -962,6 +882,11 @@ func (h *healSequence) healObject(bucket, object, versionID string) error {
|
|||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
object: object,
|
object: object,
|
||||||
versionID: versionID,
|
versionID: versionID,
|
||||||
|
opts: &h.settings,
|
||||||
}, madmin.HealItemObject)
|
}, madmin.HealItemObject)
|
||||||
|
|
||||||
|
// Wait and proceed if there are active requests
|
||||||
|
waitForLowHTTPReq()
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-31
@@ -38,13 +38,10 @@ type adminAPIHandlers struct{}
|
|||||||
|
|
||||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||||
func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||||
|
|
||||||
adminAPI := adminAPIHandlers{}
|
adminAPI := adminAPIHandlers{}
|
||||||
// Admin router
|
// Admin router
|
||||||
adminRouter := router.PathPrefix(adminPathPrefix).Subrouter()
|
adminRouter := router.PathPrefix(adminPathPrefix).Subrouter()
|
||||||
|
|
||||||
/// Service operations
|
|
||||||
|
|
||||||
adminVersions := []string{
|
adminVersions := []string{
|
||||||
adminAPIVersionPrefix,
|
adminAPIVersionPrefix,
|
||||||
}
|
}
|
||||||
@@ -69,25 +66,32 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.StorageInfoHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.StorageInfoHandler)))
|
||||||
// DataUsageInfo operations
|
// DataUsageInfo operations
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.DataUsageInfoHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(gz(httpTraceAll(adminAPI.DataUsageInfoHandler)))
|
||||||
|
// Metrics operation
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/metrics").HandlerFunc(gz(httpTraceAll(adminAPI.MetricsHandler)))
|
||||||
|
|
||||||
if globalIsDistErasure || globalIsErasure {
|
if globalIsDistErasure || globalIsErasure {
|
||||||
/// Heal operations
|
// Heal operations
|
||||||
|
|
||||||
// Heal processing endpoint.
|
// Heal processing endpoint.
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(gz(httpTraceAll(adminAPI.HealHandler)))
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(gz(httpTraceAll(adminAPI.BackgroundHealStatusHandler)))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(gz(httpTraceAll(adminAPI.BackgroundHealStatusHandler)))
|
||||||
|
|
||||||
/// Health operations
|
// Pool operations
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/pools/list").HandlerFunc(gz(httpTraceAll(adminAPI.ListPools)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/pools/status").HandlerFunc(gz(httpTraceAll(adminAPI.StatusPool))).Queries("pool", "{pool:.*}")
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/decommission").HandlerFunc(gz(httpTraceAll(adminAPI.StartDecommission))).Queries("pool", "{pool:.*}")
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/cancel").HandlerFunc(gz(httpTraceAll(adminAPI.CancelDecommission))).Queries("pool", "{pool:.*}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Profiling operations
|
// Profiling operations - deprecated API
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(gz(httpTraceAll(adminAPI.StartProfilingHandler))).
|
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(gz(httpTraceAll(adminAPI.StartProfilingHandler))).
|
||||||
Queries("profilerType", "{profilerType:.*}")
|
Queries("profilerType", "{profilerType:.*}")
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(gz(httpTraceAll(adminAPI.DownloadProfilingHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(gz(httpTraceAll(adminAPI.DownloadProfilingHandler)))
|
||||||
|
// Profiling operations
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/profile").HandlerFunc(gz(httpTraceAll(adminAPI.ProfileHandler)))
|
||||||
|
|
||||||
// Config KV operations.
|
// Config KV operations.
|
||||||
if enableConfigOps {
|
if enableConfigOps {
|
||||||
@@ -106,7 +110,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler))).Queries("restoreId", "{restoreId:.*}")
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(gz(httpTraceHdrs(adminAPI.RestoreConfigHistoryKVHandler))).Queries("restoreId", "{restoreId:.*}")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Config import/export bulk operations
|
// Config import/export bulk operations
|
||||||
if enableConfigOps {
|
if enableConfigOps {
|
||||||
// Get config
|
// Get config
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetConfigHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetConfigHandler)))
|
||||||
@@ -168,30 +172,69 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
// Set Group Status
|
// Set Group Status
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||||
|
|
||||||
if globalIsDistErasure || globalIsErasure {
|
// Export IAM info to zipped file
|
||||||
// GetBucketQuotaConfig
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-iam").HandlerFunc(httpTraceHdrs(adminAPI.ExportIAM))
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
|
||||||
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
|
||||||
// PutBucketQuotaConfig
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
|
||||||
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
|
||||||
|
|
||||||
// Bucket replication operations
|
// Import IAM info
|
||||||
// GetBucketTargetHandler
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(httpTraceHdrs(adminAPI.ImportIAM))
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
|
|
||||||
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
|
|
||||||
// SetRemoteTargetHandler
|
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
|
|
||||||
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
|
|
||||||
// RemoveRemoteTargetHandler
|
|
||||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
|
||||||
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
|
||||||
|
|
||||||
// Remote Tier management operations
|
// IDentity Provider configuration APIs
|
||||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.GetIdentityProviderCfg))).Queries("type", "{type:.*}")
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/idp-config").HandlerFunc(gz(httpTraceHdrs(adminAPI.DeleteIdentityProviderCfg))).Queries("type", "{type:.*}").Queries("name", "{name:.*}")
|
||||||
}
|
|
||||||
|
// -- END IAM APIs --
|
||||||
|
|
||||||
|
// GetBucketQuotaConfig
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||||
|
// PutBucketQuotaConfig
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||||
|
|
||||||
|
// Bucket replication operations
|
||||||
|
// GetBucketTargetHandler
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
|
||||||
|
// SetRemoteTargetHandler
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
|
||||||
|
// RemoveRemoteTargetHandler
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||||
|
|
||||||
|
// Bucket migration operations
|
||||||
|
// ExportBucketMetaHandler
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-bucket-metadata").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.ExportBucketMetadataHandler)))
|
||||||
|
// ImportBucketMetaHandler
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-bucket-metadata").HandlerFunc(
|
||||||
|
gz(httpTraceHdrs(adminAPI.ImportBucketMetadataHandler)))
|
||||||
|
|
||||||
|
// Remote Tier management operations
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
|
||||||
|
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
|
||||||
|
// Tier stats
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
|
||||||
|
|
||||||
|
// Cluster Replication APIs
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationRemove)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
|
||||||
|
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
|
||||||
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
||||||
|
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
||||||
|
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
// Top locks
|
// Top locks
|
||||||
@@ -201,7 +244,10 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
Queries("paths", "{paths:.*}").HandlerFunc(gz(httpTraceHdrs(adminAPI.ForceUnlockHandler)))
|
Queries("paths", "{paths:.*}").HandlerFunc(gz(httpTraceHdrs(adminAPI.ForceUnlockHandler)))
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedtestHandler))
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(httpTraceHdrs(adminAPI.SpeedTestHandler))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(httpTraceHdrs(adminAPI.ObjectSpeedTestHandler))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(httpTraceHdrs(adminAPI.DriveSpeedtestHandler))
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(httpTraceHdrs(adminAPI.NetperfHandler))
|
||||||
|
|
||||||
// HTTP Trace
|
// HTTP Trace
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import (
|
|||||||
// local endpoints from given list of endpoints
|
// local endpoints from given list of endpoints
|
||||||
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
||||||
var localEndpoints Endpoints
|
var localEndpoints Endpoints
|
||||||
addr := r.Host
|
addr := globalLocalNodeName
|
||||||
|
if r != nil {
|
||||||
|
addr = r.Host
|
||||||
|
}
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
addr = globalLocalNodeName
|
addr = globalLocalNodeName
|
||||||
}
|
}
|
||||||
@@ -40,7 +43,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
for _, endpoint := range ep.Endpoints {
|
for _, endpoint := range ep.Endpoints {
|
||||||
nodeName := endpoint.Host
|
nodeName := endpoint.Host
|
||||||
if nodeName == "" {
|
if nodeName == "" {
|
||||||
nodeName = r.Host
|
nodeName = addr
|
||||||
}
|
}
|
||||||
if endpoint.IsLocal {
|
if endpoint.IsLocal {
|
||||||
// Only proceed for local endpoints
|
// Only proceed for local endpoints
|
||||||
@@ -50,7 +53,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
|||||||
}
|
}
|
||||||
_, present := network[nodeName]
|
_, present := network[nodeName]
|
||||||
if !present {
|
if !present {
|
||||||
if err := isServerResolvable(endpoint, 2*time.Second); err == nil {
|
if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
|
||||||
network[nodeName] = string(madmin.ItemOnline)
|
network[nodeName] = string(madmin.ItemOnline)
|
||||||
} else {
|
} else {
|
||||||
network[nodeName] = string(madmin.ItemOffline)
|
network[nodeName] = string(madmin.ItemOffline)
|
||||||
|
|||||||
+14
-11
@@ -28,14 +28,10 @@ type DeletedObject struct {
|
|||||||
DeleteMarkerVersionID string `xml:"DeleteMarkerVersionId,omitempty"`
|
DeleteMarkerVersionID string `xml:"DeleteMarkerVersionId,omitempty"`
|
||||||
ObjectName string `xml:"Key,omitempty"`
|
ObjectName string `xml:"Key,omitempty"`
|
||||||
VersionID string `xml:"VersionId,omitempty"`
|
VersionID string `xml:"VersionId,omitempty"`
|
||||||
|
|
||||||
// MinIO extensions to support delete marker replication
|
|
||||||
// Replication status of DeleteMarker
|
|
||||||
DeleteMarkerReplicationStatus string `xml:"DeleteMarkerReplicationStatus,omitempty"`
|
|
||||||
// MTime of DeleteMarker on source that needs to be propagated to replica
|
// MTime of DeleteMarker on source that needs to be propagated to replica
|
||||||
DeleteMarkerMTime DeleteMarkerMTime `xml:"DeleteMarkerMTime,omitempty"`
|
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
||||||
// Status of versioned delete (of object or DeleteMarker)
|
// MinIO extensions to support delete marker replication
|
||||||
VersionPurgeStatus VersionPurgeStatusType `xml:"VersionPurgeStatus,omitempty"`
|
ReplicationState ReplicationState `xml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
||||||
@@ -52,16 +48,23 @@ func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElem
|
|||||||
return e.EncodeElement(t.Time.Format(time.RFC3339), startElement)
|
return e.EncodeElement(t.Time.Format(time.RFC3339), startElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectToDelete carries key name for the object to delete.
|
// ObjectV object version key/versionId
|
||||||
type ObjectToDelete struct {
|
type ObjectV struct {
|
||||||
ObjectName string `xml:"Key"`
|
ObjectName string `xml:"Key"`
|
||||||
VersionID string `xml:"VersionId"`
|
VersionID string `xml:"VersionId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectToDelete carries key name for the object to delete.
|
||||||
|
type ObjectToDelete struct {
|
||||||
|
ObjectV
|
||||||
// Replication status of DeleteMarker
|
// Replication status of DeleteMarker
|
||||||
DeleteMarkerReplicationStatus string `xml:"DeleteMarkerReplicationStatus"`
|
DeleteMarkerReplicationStatus string `xml:"DeleteMarkerReplicationStatus"`
|
||||||
// Status of versioned delete (of object or DeleteMarker)
|
// Status of versioned delete (of object or DeleteMarker)
|
||||||
VersionPurgeStatus VersionPurgeStatusType `xml:"VersionPurgeStatus"`
|
VersionPurgeStatus VersionPurgeStatusType `xml:"VersionPurgeStatus"`
|
||||||
// Version ID of delete marker
|
// VersionPurgeStatuses holds the internal
|
||||||
DeleteMarkerVersionID string `xml:"DeleteMarkerVersionId"`
|
VersionPurgeStatuses string `xml:"VersionPurgeStatuses"`
|
||||||
|
// ReplicateDecisionStr stringified representation of replication decision
|
||||||
|
ReplicateDecisionStr string `xml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// createBucketConfiguration container for bucket configuration request from client.
|
// createBucketConfiguration container for bucket configuration request from client.
|
||||||
|
|||||||
+151
-28
@@ -20,9 +20,11 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||||
@@ -80,6 +82,7 @@ const (
|
|||||||
ErrIncompleteBody
|
ErrIncompleteBody
|
||||||
ErrInternalError
|
ErrInternalError
|
||||||
ErrInvalidAccessKeyID
|
ErrInvalidAccessKeyID
|
||||||
|
ErrAccessKeyDisabled
|
||||||
ErrInvalidBucketName
|
ErrInvalidBucketName
|
||||||
ErrInvalidDigest
|
ErrInvalidDigest
|
||||||
ErrInvalidRange
|
ErrInvalidRange
|
||||||
@@ -107,6 +110,7 @@ const (
|
|||||||
ErrNoSuchBucketPolicy
|
ErrNoSuchBucketPolicy
|
||||||
ErrNoSuchBucketLifecycle
|
ErrNoSuchBucketLifecycle
|
||||||
ErrNoSuchLifecycleConfiguration
|
ErrNoSuchLifecycleConfiguration
|
||||||
|
ErrInvalidLifecycleWithObjectLock
|
||||||
ErrNoSuchBucketSSEConfig
|
ErrNoSuchBucketSSEConfig
|
||||||
ErrNoSuchCORSConfiguration
|
ErrNoSuchCORSConfiguration
|
||||||
ErrNoSuchWebsiteConfiguration
|
ErrNoSuchWebsiteConfiguration
|
||||||
@@ -126,7 +130,8 @@ const (
|
|||||||
ErrReplicationSourceNotVersionedError
|
ErrReplicationSourceNotVersionedError
|
||||||
ErrReplicationNeedsVersioningError
|
ErrReplicationNeedsVersioningError
|
||||||
ErrReplicationBucketNeedsVersioningError
|
ErrReplicationBucketNeedsVersioningError
|
||||||
ErrReplicationNoMatchingRuleError
|
ErrReplicationDenyEditError
|
||||||
|
ErrReplicationNoExistingObjects
|
||||||
ErrObjectRestoreAlreadyInProgress
|
ErrObjectRestoreAlreadyInProgress
|
||||||
ErrNoSuchKey
|
ErrNoSuchKey
|
||||||
ErrNoSuchUpload
|
ErrNoSuchUpload
|
||||||
@@ -207,6 +212,7 @@ const (
|
|||||||
ErrInvalidSSECustomerParameters
|
ErrInvalidSSECustomerParameters
|
||||||
ErrIncompatibleEncryptionMethod
|
ErrIncompatibleEncryptionMethod
|
||||||
ErrKMSNotConfigured
|
ErrKMSNotConfigured
|
||||||
|
ErrKMSKeyNotFoundException
|
||||||
|
|
||||||
ErrNoAccessKey
|
ErrNoAccessKey
|
||||||
ErrInvalidToken
|
ErrInvalidToken
|
||||||
@@ -261,10 +267,23 @@ const (
|
|||||||
ErrAdminConfigNoQuorum
|
ErrAdminConfigNoQuorum
|
||||||
ErrAdminConfigTooLarge
|
ErrAdminConfigTooLarge
|
||||||
ErrAdminConfigBadJSON
|
ErrAdminConfigBadJSON
|
||||||
|
ErrAdminNoSuchConfigTarget
|
||||||
|
ErrAdminConfigEnvOverridden
|
||||||
ErrAdminConfigDuplicateKeys
|
ErrAdminConfigDuplicateKeys
|
||||||
ErrAdminCredentialsMismatch
|
ErrAdminCredentialsMismatch
|
||||||
ErrInsecureClientRequest
|
ErrInsecureClientRequest
|
||||||
ErrObjectTampered
|
ErrObjectTampered
|
||||||
|
|
||||||
|
// Site-Replication errors
|
||||||
|
ErrSiteReplicationInvalidRequest
|
||||||
|
ErrSiteReplicationPeerResp
|
||||||
|
ErrSiteReplicationBackendIssue
|
||||||
|
ErrSiteReplicationServiceAccountError
|
||||||
|
ErrSiteReplicationBucketConfigError
|
||||||
|
ErrSiteReplicationBucketMetaError
|
||||||
|
ErrSiteReplicationIAMError
|
||||||
|
ErrSiteReplicationConfigMissing
|
||||||
|
|
||||||
// Bucket Quota error codes
|
// Bucket Quota error codes
|
||||||
ErrAdminBucketQuotaExceeded
|
ErrAdminBucketQuotaExceeded
|
||||||
ErrAdminNoSuchQuotaConfiguration
|
ErrAdminNoSuchQuotaConfiguration
|
||||||
@@ -367,6 +386,7 @@ const (
|
|||||||
ErrAdminProfilerNotEnabled
|
ErrAdminProfilerNotEnabled
|
||||||
ErrInvalidDecompressedSize
|
ErrInvalidDecompressedSize
|
||||||
ErrAddUserInvalidArgument
|
ErrAddUserInvalidArgument
|
||||||
|
ErrAdminResourceInvalidArgument
|
||||||
ErrAdminAccountNotEligible
|
ErrAdminAccountNotEligible
|
||||||
ErrAccountNotEligible
|
ErrAccountNotEligible
|
||||||
ErrAdminServiceAccountNotFound
|
ErrAdminServiceAccountNotFound
|
||||||
@@ -383,10 +403,10 @@ func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err)
|
||||||
}
|
}
|
||||||
if globalServerRegion != "" {
|
if globalSite.Region != "" {
|
||||||
switch errCode {
|
switch errCode {
|
||||||
case ErrAuthorizationHeaderMalformed:
|
case ErrAuthorizationHeaderMalformed:
|
||||||
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion)
|
apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||||
return apiErr
|
return apiErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +462,7 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrInvalidMaxParts: {
|
ErrInvalidMaxParts: {
|
||||||
Code: "InvalidArgument",
|
Code: "InvalidArgument",
|
||||||
Description: "Argument max-parts must be an integer between 0 and 2147483647",
|
Description: "Part number must be an integer between 1 and 10000, inclusive",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidPartNumberMarker: {
|
ErrInvalidPartNumberMarker: {
|
||||||
@@ -500,6 +520,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "The Access Key Id you provided does not exist in our records.",
|
Description: "The Access Key Id you provided does not exist in our records.",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
ErrAccessKeyDisabled: {
|
||||||
|
Code: "InvalidAccessKeyId",
|
||||||
|
Description: "Your account is disabled; please contact your administrator.",
|
||||||
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
ErrInvalidBucketName: {
|
ErrInvalidBucketName: {
|
||||||
Code: "InvalidBucketName",
|
Code: "InvalidBucketName",
|
||||||
Description: "The specified bucket is not valid.",
|
Description: "The specified bucket is not valid.",
|
||||||
@@ -565,6 +590,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "The lifecycle configuration does not exist",
|
Description: "The lifecycle configuration does not exist",
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
ErrInvalidLifecycleWithObjectLock: {
|
||||||
|
Code: "InvalidLifecycleWithObjectLock",
|
||||||
|
Description: "The lifecycle configuration containing MaxNoncurrentVersions is not supported with object locking",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrNoSuchBucketSSEConfig: {
|
ErrNoSuchBucketSSEConfig: {
|
||||||
Code: "ServerSideEncryptionConfigurationNotFoundError",
|
Code: "ServerSideEncryptionConfigurationNotFoundError",
|
||||||
Description: "The server side encryption configuration was not found",
|
Description: "The server side encryption configuration was not found",
|
||||||
@@ -662,7 +692,7 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrAllAccessDisabled: {
|
ErrAllAccessDisabled: {
|
||||||
Code: "AllAccessDisabled",
|
Code: "AllAccessDisabled",
|
||||||
Description: "All access to this bucket has been disabled.",
|
Description: "All access to this resource has been disabled.",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
ErrMalformedPolicy: {
|
ErrMalformedPolicy: {
|
||||||
@@ -865,9 +895,14 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Bandwidth limit for remote target must be atleast 100MBps",
|
Description: "Bandwidth limit for remote target must be atleast 100MBps",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrReplicationNoMatchingRuleError: {
|
ErrReplicationNoExistingObjects: {
|
||||||
Code: "XMinioReplicationNoMatchingRule",
|
Code: "XMinioReplicationNoExistingObjects",
|
||||||
Description: "No matching replication rule found for this object prefix",
|
Description: "No matching ExistingsObjects rule enabled",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrReplicationDenyEditError: {
|
||||||
|
Code: "XMinioReplicationDenyEdit",
|
||||||
|
Description: "Cannot alter local replication config since this server is in a cluster replication setup",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrBucketRemoteIdenticalToSource: {
|
ErrBucketRemoteIdenticalToSource: {
|
||||||
@@ -961,7 +996,7 @@ var errorCodes = errorCodeMap{
|
|||||||
HTTPStatusCode: http.StatusNotFound,
|
HTTPStatusCode: http.StatusNotFound,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Bucket notification related errors.
|
// Bucket notification related errors.
|
||||||
ErrEventNotification: {
|
ErrEventNotification: {
|
||||||
Code: "InvalidArgument",
|
Code: "InvalidArgument",
|
||||||
Description: "A specified event is not supported for notifications.",
|
Description: "A specified event is not supported for notifications.",
|
||||||
@@ -1097,6 +1132,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "Server side encryption specified but KMS is not configured",
|
Description: "Server side encryption specified but KMS is not configured",
|
||||||
HTTPStatusCode: http.StatusNotImplemented,
|
HTTPStatusCode: http.StatusNotImplemented,
|
||||||
},
|
},
|
||||||
|
ErrKMSKeyNotFoundException: {
|
||||||
|
Code: "KMS.NotFoundException",
|
||||||
|
Description: "Invalid keyId",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrNoAccessKey: {
|
ErrNoAccessKey: {
|
||||||
Code: "AccessDenied",
|
Code: "AccessDenied",
|
||||||
Description: "No AWSAccessKey was presented",
|
Description: "No AWSAccessKey was presented",
|
||||||
@@ -1108,14 +1148,14 @@ var errorCodes = errorCodeMap{
|
|||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// S3 extensions.
|
// S3 extensions.
|
||||||
ErrContentSHA256Mismatch: {
|
ErrContentSHA256Mismatch: {
|
||||||
Code: "XAmzContentSHA256Mismatch",
|
Code: "XAmzContentSHA256Mismatch",
|
||||||
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
|
Description: "The provided 'x-amz-content-sha256' header does not match what was computed.",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// MinIO extensions.
|
// MinIO extensions.
|
||||||
ErrStorageFull: {
|
ErrStorageFull: {
|
||||||
Code: "XMinioStorageFull",
|
Code: "XMinioStorageFull",
|
||||||
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.",
|
||||||
@@ -1129,7 +1169,7 @@ var errorCodes = errorCodeMap{
|
|||||||
ErrObjectExistsAsDirectory: {
|
ErrObjectExistsAsDirectory: {
|
||||||
Code: "XMinioObjectExistsAsDirectory",
|
Code: "XMinioObjectExistsAsDirectory",
|
||||||
Description: "Object name already exists as a directory.",
|
Description: "Object name already exists as a directory.",
|
||||||
HTTPStatusCode: http.StatusConflict,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrInvalidObjectName: {
|
ErrInvalidObjectName: {
|
||||||
Code: "XMinioInvalidObjectName",
|
Code: "XMinioInvalidObjectName",
|
||||||
@@ -1202,11 +1242,21 @@ var errorCodes = errorCodeMap{
|
|||||||
maxEConfigJSONSize),
|
maxEConfigJSONSize),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminNoSuchConfigTarget: {
|
||||||
|
Code: "XMinioAdminNoSuchConfigTarget",
|
||||||
|
Description: "No such named configuration target exists",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminConfigBadJSON: {
|
ErrAdminConfigBadJSON: {
|
||||||
Code: "XMinioAdminConfigBadJSON",
|
Code: "XMinioAdminConfigBadJSON",
|
||||||
Description: "JSON configuration provided is of incorrect format",
|
Description: "JSON configuration provided is of incorrect format",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrAdminConfigEnvOverridden: {
|
||||||
|
Code: "XMinioAdminConfigEnvOverridden",
|
||||||
|
Description: "Unable to update config via Admin API due to environment variable override",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminConfigDuplicateKeys: {
|
ErrAdminConfigDuplicateKeys: {
|
||||||
Code: "XMinioAdminConfigDuplicateKeys",
|
Code: "XMinioAdminConfigDuplicateKeys",
|
||||||
Description: "JSON configuration provided has objects with duplicate keys",
|
Description: "JSON configuration provided has objects with duplicate keys",
|
||||||
@@ -1267,6 +1317,47 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: errObjectTampered.Error(),
|
Description: errObjectTampered.Error(),
|
||||||
HTTPStatusCode: http.StatusPartialContent,
|
HTTPStatusCode: http.StatusPartialContent,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ErrSiteReplicationInvalidRequest: {
|
||||||
|
Code: "XMinioSiteReplicationInvalidRequest",
|
||||||
|
Description: "Invalid site-replication request",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationPeerResp: {
|
||||||
|
Code: "XMinioSiteReplicationPeerResp",
|
||||||
|
Description: "Error received when contacting a peer site",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationBackendIssue: {
|
||||||
|
Code: "XMinioSiteReplicationBackendIssue",
|
||||||
|
Description: "Error when requesting object layer backend",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationServiceAccountError: {
|
||||||
|
Code: "XMinioSiteReplicationServiceAccountError",
|
||||||
|
Description: "Site replication related service account error",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationBucketConfigError: {
|
||||||
|
Code: "XMinioSiteReplicationBucketConfigError",
|
||||||
|
Description: "Error while configuring replication on a bucket",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationBucketMetaError: {
|
||||||
|
Code: "XMinioSiteReplicationBucketMetaError",
|
||||||
|
Description: "Error while replicating bucket metadata",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationIAMError: {
|
||||||
|
Code: "XMinioSiteReplicationIAMError",
|
||||||
|
Description: "Error while replicating an IAM item",
|
||||||
|
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||||
|
},
|
||||||
|
ErrSiteReplicationConfigMissing: {
|
||||||
|
Code: "XMinioSiteReplicationConfigMissingError",
|
||||||
|
Description: "Site not found in site replication configuration",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrMaximumExpires: {
|
ErrMaximumExpires: {
|
||||||
Code: "AuthorizationQueryParametersError",
|
Code: "AuthorizationQueryParametersError",
|
||||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||||
@@ -1313,15 +1404,15 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrBackendDown: {
|
ErrBackendDown: {
|
||||||
Code: "XMinioBackendDown",
|
Code: "XMinioBackendDown",
|
||||||
Description: "Object storage backend is unreachable",
|
Description: "Remote backend is unreachable",
|
||||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrIncorrectContinuationToken: {
|
ErrIncorrectContinuationToken: {
|
||||||
Code: "InvalidArgument",
|
Code: "InvalidArgument",
|
||||||
Description: "The continuation token provided is incorrect",
|
Description: "The continuation token provided is incorrect",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
//S3 Select API Errors
|
// S3 Select API Errors
|
||||||
ErrEmptyRequestBody: {
|
ErrEmptyRequestBody: {
|
||||||
Code: "EmptyRequestBody",
|
Code: "EmptyRequestBody",
|
||||||
Description: "Request body cannot be empty.",
|
Description: "Request body cannot be empty.",
|
||||||
@@ -1752,6 +1843,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "User is not allowed to be same as admin access key",
|
Description: "User is not allowed to be same as admin access key",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
ErrAdminResourceInvalidArgument: {
|
||||||
|
Code: "XMinioInvalidResource",
|
||||||
|
Description: "Policy, user or group names are not allowed to begin or end with space characters",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrAdminAccountNotEligible: {
|
ErrAdminAccountNotEligible: {
|
||||||
Code: "XMinioInvalidIAMCredentials",
|
Code: "XMinioInvalidIAMCredentials",
|
||||||
Description: "The administrator key is not eligible for this operation",
|
Description: "The administrator key is not eligible for this operation",
|
||||||
@@ -1785,12 +1881,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
|
|
||||||
// Only return ErrClientDisconnected if the provided context is actually canceled.
|
// Only return ErrClientDisconnected if the provided context is actually canceled.
|
||||||
// This way downstream context.Canceled will still report ErrOperationTimedOut
|
// This way downstream context.Canceled will still report ErrOperationTimedOut
|
||||||
select {
|
if contextCanceled(ctx) {
|
||||||
case <-ctx.Done():
|
|
||||||
if ctx.Err() == context.Canceled {
|
if ctx.Err() == context.Canceled {
|
||||||
return ErrClientDisconnected
|
return ErrClientDisconnected
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch err {
|
switch err {
|
||||||
@@ -1847,6 +1941,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrIncompatibleEncryptionMethod
|
apiErr = ErrIncompatibleEncryptionMethod
|
||||||
case errKMSNotConfigured:
|
case errKMSNotConfigured:
|
||||||
apiErr = ErrKMSNotConfigured
|
apiErr = ErrKMSNotConfigured
|
||||||
|
case errKMSKeyNotFound:
|
||||||
|
apiErr = ErrKMSKeyNotFoundException
|
||||||
|
|
||||||
case context.Canceled, context.DeadlineExceeded:
|
case context.Canceled, context.DeadlineExceeded:
|
||||||
apiErr = ErrOperationTimedOut
|
apiErr = ErrOperationTimedOut
|
||||||
case errDiskNotFound:
|
case errDiskNotFound:
|
||||||
@@ -1909,6 +2006,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrNoSuchKey
|
apiErr = ErrNoSuchKey
|
||||||
case MethodNotAllowed:
|
case MethodNotAllowed:
|
||||||
apiErr = ErrMethodNotAllowed
|
apiErr = ErrMethodNotAllowed
|
||||||
|
case ObjectLocked:
|
||||||
|
apiErr = ErrObjectLocked
|
||||||
case InvalidVersionID:
|
case InvalidVersionID:
|
||||||
apiErr = ErrInvalidVersionID
|
apiErr = ErrInvalidVersionID
|
||||||
case VersionNotFound:
|
case VersionNotFound:
|
||||||
@@ -1965,8 +2064,6 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrReplicationConfigurationNotFoundError
|
apiErr = ErrReplicationConfigurationNotFoundError
|
||||||
case BucketRemoteDestinationNotFound:
|
case BucketRemoteDestinationNotFound:
|
||||||
apiErr = ErrRemoteDestinationNotFoundError
|
apiErr = ErrRemoteDestinationNotFoundError
|
||||||
case BucketReplicationDestinationMissingLock:
|
|
||||||
apiErr = ErrReplicationDestinationMissingLock
|
|
||||||
case BucketRemoteTargetNotFound:
|
case BucketRemoteTargetNotFound:
|
||||||
apiErr = ErrRemoteTargetNotFoundError
|
apiErr = ErrRemoteTargetNotFoundError
|
||||||
case BucketRemoteConnectionErr:
|
case BucketRemoteConnectionErr:
|
||||||
@@ -2027,6 +2124,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
default:
|
default:
|
||||||
var ie, iw int
|
var ie, iw int
|
||||||
// This work-around is to handle the issue golang/go#30648
|
// This work-around is to handle the issue golang/go#30648
|
||||||
|
//nolint:gocritic
|
||||||
if _, ferr := fmt.Fscanf(strings.NewReader(err.Error()),
|
if _, ferr := fmt.Fscanf(strings.NewReader(err.Error()),
|
||||||
"request declared a Content-Length of %d but only wrote %d bytes",
|
"request declared a Content-Length of %d but only wrote %d bytes",
|
||||||
&ie, &iw); ferr != nil {
|
&ie, &iw); ferr != nil {
|
||||||
@@ -2059,7 +2157,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
return noError
|
return noError
|
||||||
}
|
}
|
||||||
|
|
||||||
var apiErr = errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
apiErr := errorCodes.ToAPIErr(toAPIErrorCode(ctx, err))
|
||||||
e, ok := err.(dns.ErrInvalidBucketName)
|
e, ok := err.(dns.ErrInvalidBucketName)
|
||||||
if ok {
|
if ok {
|
||||||
code := toAPIErrorCode(ctx, e)
|
code := toAPIErrorCode(ctx, e)
|
||||||
@@ -2082,6 +2180,11 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if apiErr.Code == "XMinioBackendDown" {
|
||||||
|
apiErr.Description = fmt.Sprintf("%s (%v)", apiErr.Description, err)
|
||||||
|
return apiErr
|
||||||
|
}
|
||||||
|
|
||||||
if apiErr.Code == "InternalError" {
|
if apiErr.Code == "InternalError" {
|
||||||
// If we see an internal error try to interpret
|
// If we see an internal error try to interpret
|
||||||
// any underlying errors if possible depending on
|
// any underlying errors if possible depending on
|
||||||
@@ -2140,7 +2243,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
case crypto.Error:
|
case crypto.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Code: "XMinIOEncryptionError",
|
Code: "XMinioEncryptionError",
|
||||||
Description: e.Error(),
|
Description: e.Error(),
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
}
|
}
|
||||||
@@ -2167,7 +2270,6 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
// since S3 only sends one Error XML response.
|
// since S3 only sends one Error XML response.
|
||||||
if len(e.Errors) >= 1 {
|
if len(e.Errors) >= 1 {
|
||||||
apiErr.Code = e.Errors[0].Reason
|
apiErr.Code = e.Errors[0].Reason
|
||||||
|
|
||||||
}
|
}
|
||||||
case azblob.StorageError:
|
case azblob.StorageError:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
@@ -2177,10 +2279,31 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
}
|
}
|
||||||
// Add more Gateway SDKs here if any in future.
|
// Add more Gateway SDKs here if any in future.
|
||||||
default:
|
default:
|
||||||
apiErr = APIError{
|
//nolint:gocritic
|
||||||
Code: apiErr.Code,
|
if errors.Is(err, errMalformedEncoding) {
|
||||||
Description: fmt.Sprintf("%s: cause(%v)", apiErr.Description, err),
|
apiErr = APIError{
|
||||||
HTTPStatusCode: apiErr.HTTPStatusCode,
|
Code: "BadRequest",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
} else if errors.Is(err, errChunkTooBig) {
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "BadRequest",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
} else if errors.Is(err, strconv.ErrRange) {
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: "BadRequest",
|
||||||
|
Description: err.Error(),
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiErr = APIError{
|
||||||
|
Code: apiErr.Code,
|
||||||
|
Description: fmt.Sprintf("%s: cause(%v)", apiErr.Description, err),
|
||||||
|
HTTPStatusCode: apiErr.HTTPStatusCode,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2206,7 +2329,7 @@ func getAPIErrorResponse(ctx context.Context, err APIError, resource, requestID,
|
|||||||
BucketName: reqInfo.BucketName,
|
BucketName: reqInfo.BucketName,
|
||||||
Key: reqInfo.ObjectName,
|
Key: reqInfo.ObjectName,
|
||||||
Resource: resource,
|
Resource: resource,
|
||||||
Region: globalServerRegion,
|
Region: globalSite.Region,
|
||||||
RequestID: requestID,
|
RequestID: requestID,
|
||||||
HostID: hostID,
|
HostID: hostID,
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -52,7 +52,7 @@ func setCommonHeaders(w http.ResponseWriter) {
|
|||||||
|
|
||||||
// Set `x-amz-bucket-region` only if region is set on the server
|
// Set `x-amz-bucket-region` only if region is set on the server
|
||||||
// by default minio uses an empty region.
|
// by default minio uses an empty region.
|
||||||
if region := globalServerRegion; region != "" {
|
if region := globalSite.Region; region != "" {
|
||||||
w.Header().Set(xhttp.AmzBucketRegion, region)
|
w.Header().Set(xhttp.AmzBucketRegion, region)
|
||||||
}
|
}
|
||||||
w.Header().Set(xhttp.AcceptRanges, "bytes")
|
w.Header().Set(xhttp.AcceptRanges, "bytes")
|
||||||
@@ -126,6 +126,11 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
|||||||
|
|
||||||
// Set all other user defined metadata.
|
// Set all other user defined metadata.
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
|
// Empty values for object lock and retention can be skipped.
|
||||||
|
if v == "" && equals(k, xhttp.AmzObjectLockMode, xhttp.AmzObjectLockRetainUntilDate) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
// Do not need to send any internal metadata
|
// Do not need to send any internal metadata
|
||||||
// values to client.
|
// values to client.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
func TestNewRequestID(t *testing.T) {
|
func TestNewRequestID(t *testing.T) {
|
||||||
// Ensure that it returns an alphanumeric result of length 16.
|
// Ensure that it returns an alphanumeric result of length 16.
|
||||||
var id = mustGetRequestID(UTCNow())
|
id := mustGetRequestID(UTCNow())
|
||||||
|
|
||||||
if len(id) != 16 {
|
if len(id) != 16 {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
|
|||||||
+47
-29
@@ -29,6 +29,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/crypto"
|
||||||
"github.com/minio/minio/internal/handlers"
|
"github.com/minio/minio/internal/handlers"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
@@ -38,7 +39,7 @@ const (
|
|||||||
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
|
||||||
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision.
|
||||||
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
|
||||||
maxDeleteList = 10000 // Limit number of objects deleted in a delete call.
|
maxDeleteList = 1000 // Limit number of objects deleted in a delete call.
|
||||||
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse.
|
||||||
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
maxPartsList = 10000 // Limit number of parts in a listPartsResponse.
|
||||||
)
|
)
|
||||||
@@ -262,12 +263,11 @@ func (o ObjectVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error
|
|||||||
return e.EncodeElement(objectVersionWrapper(o), start)
|
return e.EncodeElement(objectVersionWrapper(o), start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringMap is a map[string]string.
|
// StringMap is a map[string]string
|
||||||
type StringMap map[string]string
|
type StringMap map[string]string
|
||||||
|
|
||||||
// MarshalXML - StringMap marshals into XML.
|
// MarshalXML - StringMap marshals into XML.
|
||||||
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
|
||||||
tokens := []xml.Token{start}
|
tokens := []xml.Token{start}
|
||||||
|
|
||||||
for key, value := range s {
|
for key, value := range s {
|
||||||
@@ -416,17 +416,17 @@ func getObjectLocation(r *http.Request, domains []string, bucket, object string)
|
|||||||
// serialized to match XML and JSON API spec output.
|
// serialized to match XML and JSON API spec output.
|
||||||
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
||||||
listbuckets := make([]Bucket, 0, len(buckets))
|
listbuckets := make([]Bucket, 0, len(buckets))
|
||||||
var data = ListBucketsResponse{}
|
data := ListBucketsResponse{}
|
||||||
var owner = Owner{
|
owner := Owner{
|
||||||
ID: globalMinioDefaultOwnerID,
|
ID: globalMinioDefaultOwnerID,
|
||||||
DisplayName: "minio",
|
DisplayName: "minio",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
var listbucket = Bucket{}
|
listbuckets = append(listbuckets, Bucket{
|
||||||
listbucket.Name = bucket.Name
|
Name: bucket.Name,
|
||||||
listbucket.CreationDate = bucket.Created.UTC().Format(iso8601TimeFormat)
|
CreationDate: bucket.Created.UTC().Format(iso8601TimeFormat),
|
||||||
listbuckets = append(listbuckets, listbucket)
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Owner = owner
|
data.Owner = owner
|
||||||
@@ -438,14 +438,14 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse {
|
|||||||
// generates an ListBucketVersions response for the said bucket with other enumerated options.
|
// generates an ListBucketVersions response for the said bucket with other enumerated options.
|
||||||
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
|
func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse {
|
||||||
versions := make([]ObjectVersion, 0, len(resp.Objects))
|
versions := make([]ObjectVersion, 0, len(resp.Objects))
|
||||||
var owner = Owner{
|
owner := Owner{
|
||||||
ID: globalMinioDefaultOwnerID,
|
ID: globalMinioDefaultOwnerID,
|
||||||
DisplayName: "minio",
|
DisplayName: "minio",
|
||||||
}
|
}
|
||||||
var data = ListVersionsResponse{}
|
data := ListVersionsResponse{}
|
||||||
|
|
||||||
for _, object := range resp.Objects {
|
for _, object := range resp.Objects {
|
||||||
var content = ObjectVersion{}
|
content := ObjectVersion{}
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -485,7 +485,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
|||||||
|
|
||||||
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
|
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
|
||||||
for _, prefix := range resp.Prefixes {
|
for _, prefix := range resp.Prefixes {
|
||||||
var prefixItem = CommonPrefix{}
|
prefixItem := CommonPrefix{}
|
||||||
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
||||||
prefixes = append(prefixes, prefixItem)
|
prefixes = append(prefixes, prefixItem)
|
||||||
}
|
}
|
||||||
@@ -496,14 +496,14 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
|||||||
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
|
// generates an ListObjectsV1 response for the said bucket with other enumerated options.
|
||||||
func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
|
func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse {
|
||||||
contents := make([]Object, 0, len(resp.Objects))
|
contents := make([]Object, 0, len(resp.Objects))
|
||||||
var owner = Owner{
|
owner := Owner{
|
||||||
ID: globalMinioDefaultOwnerID,
|
ID: globalMinioDefaultOwnerID,
|
||||||
DisplayName: "minio",
|
DisplayName: "minio",
|
||||||
}
|
}
|
||||||
var data = ListObjectsResponse{}
|
data := ListObjectsResponse{}
|
||||||
|
|
||||||
for _, object := range resp.Objects {
|
for _, object := range resp.Objects {
|
||||||
var content = Object{}
|
content := Object{}
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -534,7 +534,7 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
|||||||
|
|
||||||
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
|
prefixes := make([]CommonPrefix, 0, len(resp.Prefixes))
|
||||||
for _, prefix := range resp.Prefixes {
|
for _, prefix := range resp.Prefixes {
|
||||||
var prefixItem = CommonPrefix{}
|
prefixItem := CommonPrefix{}
|
||||||
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
||||||
prefixes = append(prefixes, prefixItem)
|
prefixes = append(prefixes, prefixItem)
|
||||||
}
|
}
|
||||||
@@ -545,14 +545,14 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
|||||||
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
// generates an ListObjectsV2 response for the said bucket with other enumerated options.
|
||||||
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata bool) ListObjectsV2Response {
|
func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata bool) ListObjectsV2Response {
|
||||||
contents := make([]Object, 0, len(objects))
|
contents := make([]Object, 0, len(objects))
|
||||||
var owner = Owner{
|
owner := Owner{
|
||||||
ID: globalMinioDefaultOwnerID,
|
ID: globalMinioDefaultOwnerID,
|
||||||
DisplayName: "minio",
|
DisplayName: "minio",
|
||||||
}
|
}
|
||||||
var data = ListObjectsV2Response{}
|
data := ListObjectsV2Response{}
|
||||||
|
|
||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
var content = Object{}
|
content := Object{}
|
||||||
if object.Name == "" {
|
if object.Name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -570,6 +570,14 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
content.Owner = owner
|
content.Owner = owner
|
||||||
if metadata {
|
if metadata {
|
||||||
content.UserMetadata = make(StringMap)
|
content.UserMetadata = make(StringMap)
|
||||||
|
switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind {
|
||||||
|
case crypto.S3:
|
||||||
|
content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
||||||
|
case crypto.S3KMS:
|
||||||
|
content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionKMS
|
||||||
|
case crypto.SSEC:
|
||||||
|
content.UserMetadata[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
|
||||||
|
}
|
||||||
for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) {
|
for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) {
|
||||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||||
// Do not need to send any internal metadata
|
// Do not need to send any internal metadata
|
||||||
@@ -599,7 +607,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
|||||||
|
|
||||||
commonPrefixes := make([]CommonPrefix, 0, len(prefixes))
|
commonPrefixes := make([]CommonPrefix, 0, len(prefixes))
|
||||||
for _, prefix := range prefixes {
|
for _, prefix := range prefixes {
|
||||||
var prefixItem = CommonPrefix{}
|
prefixItem := CommonPrefix{}
|
||||||
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
prefixItem.Prefix = s3EncodeName(prefix, encodingType)
|
||||||
commonPrefixes = append(commonPrefixes, prefixItem)
|
commonPrefixes = append(commonPrefixes, prefixItem)
|
||||||
}
|
}
|
||||||
@@ -715,14 +723,19 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
|
|||||||
if !quiet {
|
if !quiet {
|
||||||
deleteResp.DeletedObjects = deletedObjects
|
deleteResp.DeletedObjects = deletedObjects
|
||||||
}
|
}
|
||||||
if len(errs) == len(deletedObjects) {
|
|
||||||
deleteResp.DeletedObjects = nil
|
|
||||||
}
|
|
||||||
deleteResp.Errors = errs
|
deleteResp.Errors = errs
|
||||||
return deleteResp
|
return deleteResp
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode = 200
|
||||||
|
}
|
||||||
|
// Similar check to http.checkWriteHeaderCode
|
||||||
|
if statusCode < 100 || statusCode > 999 {
|
||||||
|
logger.Error(fmt.Sprintf("invalid WriteHeader code %v", statusCode))
|
||||||
|
statusCode = http.StatusInternalServerError
|
||||||
|
}
|
||||||
setCommonHeaders(w)
|
setCommonHeaders(w)
|
||||||
if mType != mimeNone {
|
if mType != mimeNone {
|
||||||
w.Header().Set(xhttp.ContentType, string(mType))
|
w.Header().Set(xhttp.ContentType, string(mType))
|
||||||
@@ -731,7 +744,6 @@ func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType
|
|||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
w.Write(response)
|
w.Write(response)
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -782,9 +794,15 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
|||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
w.Header().Set(xhttp.RetryAfter, "120")
|
w.Header().Set(xhttp.RetryAfter, "120")
|
||||||
case "InvalidRegion":
|
case "InvalidRegion":
|
||||||
err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalServerRegion)
|
err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalSite.Region)
|
||||||
case "AuthorizationHeaderMalformed":
|
case "AuthorizationHeaderMalformed":
|
||||||
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion)
|
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar check to http.checkWriteHeaderCode
|
||||||
|
if err.HTTPStatusCode < 100 || err.HTTPStatusCode > 999 {
|
||||||
|
logger.Error(fmt.Sprintf("invalid WriteHeader code %v from %v", err.HTTPStatusCode, err.Code))
|
||||||
|
err.HTTPStatusCode = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate error response.
|
// Generate error response.
|
||||||
@@ -816,8 +834,8 @@ func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIE
|
|||||||
// but accepts the error message directly (this allows messages to be
|
// but accepts the error message directly (this allows messages to be
|
||||||
// dynamically generated.)
|
// dynamically generated.)
|
||||||
func writeCustomErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError,
|
func writeCustomErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError,
|
||||||
errBody string, reqURL *url.URL) {
|
errBody string, reqURL *url.URL,
|
||||||
|
) {
|
||||||
reqInfo := logger.GetReqInfo(ctx)
|
reqInfo := logger.GetReqInfo(ctx)
|
||||||
errorResponse := APIErrorResponse{
|
errorResponse := APIErrorResponse{
|
||||||
Code: err.Code,
|
Code: err.Code,
|
||||||
|
|||||||
+27
-10
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/klauspost/compress/gzhttp"
|
"github.com/klauspost/compress/gzhttp"
|
||||||
|
"github.com/minio/console/restapi"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/wildcard"
|
"github.com/minio/pkg/wildcard"
|
||||||
@@ -42,6 +43,18 @@ func setHTTPServer(h *xhttp.Server) {
|
|||||||
globalObjLayerMutex.Unlock()
|
globalObjLayerMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newConsoleServerFn() *restapi.Server {
|
||||||
|
globalObjLayerMutex.RLock()
|
||||||
|
defer globalObjLayerMutex.RUnlock()
|
||||||
|
return globalConsoleSrv
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConsoleSrv(srv *restapi.Server) {
|
||||||
|
globalObjLayerMutex.Lock()
|
||||||
|
globalConsoleSrv = srv
|
||||||
|
globalObjLayerMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func newObjectLayerFn() ObjectLayer {
|
func newObjectLayerFn() ObjectLayer {
|
||||||
globalObjLayerMutex.RLock()
|
globalObjLayerMutex.RLock()
|
||||||
defer globalObjLayerMutex.RUnlock()
|
defer globalObjLayerMutex.RUnlock()
|
||||||
@@ -241,7 +254,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}")
|
collectAPIStats("listobjectparts", maxClients(gz(httpTraceAll(api.ListObjectPartsHandler))))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// CompleteMultipartUpload
|
// CompleteMultipartUpload
|
||||||
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("completemutipartupload", maxClients(gz(httpTraceAll(api.CompleteMultipartUploadHandler))))).Queries("uploadId", "{uploadId:.*}")
|
collectAPIStats("completemultipartupload", maxClients(gz(httpTraceAll(api.CompleteMultipartUploadHandler))))).Queries("uploadId", "{uploadId:.*}")
|
||||||
// NewMultipartUpload
|
// NewMultipartUpload
|
||||||
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("newmultipartupload", maxClients(gz(httpTraceAll(api.NewMultipartUploadHandler))))).Queries("uploads", "")
|
collectAPIStats("newmultipartupload", maxClients(gz(httpTraceAll(api.NewMultipartUploadHandler))))).Queries("uploads", "")
|
||||||
@@ -274,7 +287,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
|
collectAPIStats("getobjectlegalhold", maxClients(gz(httpTraceAll(api.GetObjectLegalHoldHandler))))).Queries("legal-hold", "")
|
||||||
// GetObject - note gzip compression is *not* added due to Range requests.
|
// GetObject - note gzip compression is *not* added due to Range requests.
|
||||||
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("getobject", maxClients(httpTraceHdrs(api.GetObjectHandler))))
|
collectAPIStats("getobject", maxClients(gz(httpTraceHdrs(api.GetObjectHandler)))))
|
||||||
// CopyObject
|
// CopyObject
|
||||||
router.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(
|
router.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(
|
||||||
collectAPIStats("copyobject", maxClients(gz(httpTraceAll(api.CopyObjectHandler)))))
|
collectAPIStats("copyobject", maxClients(gz(httpTraceAll(api.CopyObjectHandler)))))
|
||||||
@@ -301,7 +314,8 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
router.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||||
collectAPIStats("restoreobject", maxClients(gz(httpTraceAll(api.PostRestoreObjectHandler))))).Queries("restore", "")
|
collectAPIStats("restoreobject", maxClients(gz(httpTraceAll(api.PostRestoreObjectHandler))))).Queries("restore", "")
|
||||||
|
|
||||||
/// Bucket operations
|
// Bucket operations
|
||||||
|
|
||||||
// GetBucketLocation
|
// GetBucketLocation
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("getbucketlocation", maxClients(gz(httpTraceAll(api.GetBucketLocationHandler))))).Queries("location", "")
|
collectAPIStats("getbucketlocation", maxClients(gz(httpTraceAll(api.GetBucketLocationHandler))))).Queries("location", "")
|
||||||
@@ -328,7 +342,10 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
|
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||||
|
// ResetBucketReplicationStatus - MinIO extension API
|
||||||
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
|
collectAPIStats("resetbucketreplicationstatus", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStatusHandler))))).Queries("replication-reset-status", "")
|
||||||
|
|
||||||
// Dummy Bucket Calls
|
// Dummy Bucket Calls
|
||||||
// GetBucketACL -- this is a dummy call.
|
// GetBucketACL -- this is a dummy call.
|
||||||
@@ -355,7 +372,7 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
// GetBucketTaggingHandler
|
// GetBucketTaggingHandler
|
||||||
router.Methods(http.MethodGet).HandlerFunc(
|
router.Methods(http.MethodGet).HandlerFunc(
|
||||||
collectAPIStats("getbuckettagging", maxClients(gz(httpTraceAll(api.GetBucketTaggingHandler))))).Queries("tagging", "")
|
collectAPIStats("getbuckettagging", maxClients(gz(httpTraceAll(api.GetBucketTaggingHandler))))).Queries("tagging", "")
|
||||||
//DeleteBucketWebsiteHandler
|
// DeleteBucketWebsiteHandler
|
||||||
router.Methods(http.MethodDelete).HandlerFunc(
|
router.Methods(http.MethodDelete).HandlerFunc(
|
||||||
collectAPIStats("deletebucketwebsite", maxClients(gz(httpTraceAll(api.DeleteBucketWebsiteHandler))))).Queries("website", "")
|
collectAPIStats("deletebucketwebsite", maxClients(gz(httpTraceAll(api.DeleteBucketWebsiteHandler))))).Queries("website", "")
|
||||||
// DeleteBucketTaggingHandler
|
// DeleteBucketTaggingHandler
|
||||||
@@ -403,9 +420,10 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
// PutBucketNotification
|
// PutBucketNotification
|
||||||
router.Methods(http.MethodPut).HandlerFunc(
|
router.Methods(http.MethodPut).HandlerFunc(
|
||||||
collectAPIStats("putbucketnotification", maxClients(gz(httpTraceAll(api.PutBucketNotificationHandler))))).Queries("notification", "")
|
collectAPIStats("putbucketnotification", maxClients(gz(httpTraceAll(api.PutBucketNotificationHandler))))).Queries("notification", "")
|
||||||
// ResetBucketReplicationState - MinIO extension API
|
// ResetBucketReplicationStart - MinIO extension API
|
||||||
router.Methods(http.MethodPut).HandlerFunc(
|
router.Methods(http.MethodPut).HandlerFunc(
|
||||||
collectAPIStats("resetbucketreplicationstate", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStateHandler))))).Queries("replication-reset", "")
|
collectAPIStats("resetbucketreplicationstart", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStartHandler))))).Queries("replication-reset", "")
|
||||||
|
|
||||||
// PutBucket
|
// PutBucket
|
||||||
router.Methods(http.MethodPut).HandlerFunc(
|
router.Methods(http.MethodPut).HandlerFunc(
|
||||||
collectAPIStats("putbucket", maxClients(gz(httpTraceAll(api.PutBucketHandler)))))
|
collectAPIStats("putbucket", maxClients(gz(httpTraceAll(api.PutBucketHandler)))))
|
||||||
@@ -452,11 +470,11 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
collectAPIStats("listobjectsv1", maxClients(gz(httpTraceAll(api.ListObjectsV1Handler)))))
|
collectAPIStats("listobjectsv1", maxClients(gz(httpTraceAll(api.ListObjectsV1Handler)))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Root operation
|
// Root operation
|
||||||
|
|
||||||
// ListenNotification
|
// ListenNotification
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||||
|
|
||||||
// ListBuckets
|
// ListBuckets
|
||||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||||
@@ -470,7 +488,6 @@ func registerAPIRouter(router *mux.Router) {
|
|||||||
// If none of the routes match add default error handler routes
|
// If none of the routes match add default error handler routes
|
||||||
apiRouter.NotFoundHandler = collectAPIStats("notfound", httpTraceAll(errorResponseHandler))
|
apiRouter.NotFoundHandler = collectAPIStats("notfound", httpTraceAll(errorResponseHandler))
|
||||||
apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3")))
|
apiRouter.MethodNotAllowedHandler = collectAPIStats("methodnotallowed", httpTraceAll(methodNotAllowedHandler("S3")))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// corsHandler handler for CORS (Cross Origin Resource Sharing)
|
// corsHandler handler for CORS (Cross Origin Resource Sharing)
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ func TestS3EncodeName(t *testing.T) {
|
|||||||
if testCase.expectedOutput != outputText {
|
if testCase.expectedOutput != outputText {
|
||||||
t.Errorf("Expected `%s`, got `%s`", testCase.expectedOutput, outputText)
|
t.Errorf("Expected `%s`, got `%s`", testCase.expectedOutput, outputText)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+284
-269
File diff suppressed because one or more lines are too long
+89
-37
@@ -138,7 +138,7 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
|||||||
var owner bool
|
var owner bool
|
||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
||||||
getRequestAuthType(r) == authTypeSigned && !skipContentSha256Cksum(r) {
|
getRequestAuthType(r) == authTypeSigned {
|
||||||
// We only support admin credentials to access admin APIs.
|
// We only support admin credentials to access admin APIs.
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
@@ -197,13 +197,7 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
|||||||
return claims
|
return claims
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch claims in the security token returned by the client.
|
func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{}, error) {
|
||||||
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|
||||||
if token == "" {
|
|
||||||
claims := xjwt.NewMapClaims()
|
|
||||||
return claims.Map(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT token for x-amz-security-token is signed with admin
|
// JWT token for x-amz-security-token is signed with admin
|
||||||
// secret key, temporary credentials become invalid if
|
// secret key, temporary credentials become invalid if
|
||||||
// server admin credentials change. This is done to ensure
|
// server admin credentials change. This is done to ensure
|
||||||
@@ -212,13 +206,19 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|||||||
// hijacking the policies. We need to make sure that this is
|
// hijacking the policies. We need to make sure that this is
|
||||||
// based an admin credential such that token cannot be decoded
|
// based an admin credential such that token cannot be decoded
|
||||||
// on the client side and is treated like an opaque value.
|
// on the client side and is treated like an opaque value.
|
||||||
claims, err := auth.ExtractClaims(token, globalActiveCred.SecretKey)
|
claims, err := auth.ExtractClaims(token, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errAuthentication
|
if subtle.ConstantTimeCompare([]byte(secret), []byte(globalActiveCred.SecretKey)) == 1 {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
|
claims, err = auth.ExtractClaims(token, globalActiveCred.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errAuthentication
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If OPA is set, return without any further checks.
|
// If AuthZPlugin is set, return without any further checks.
|
||||||
if globalPolicyOPA != nil {
|
if newGlobalAuthZPluginFn() != nil {
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,42 +235,56 @@ func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
|||||||
logger.LogIf(GlobalContext, err, logger.Application)
|
logger.LogIf(GlobalContext, err, logger.Application)
|
||||||
return nil, errAuthentication
|
return nil, errAuthentication
|
||||||
}
|
}
|
||||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
claims.MapClaims[sessionPolicyNameExtracted] = string(spBytes)
|
||||||
}
|
|
||||||
|
|
||||||
// If LDAP claim key is set, return here.
|
|
||||||
if _, ok := claims.MapClaims[ldapUser]; ok {
|
|
||||||
return claims.Map(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session token must have a policy, reject requests without policy
|
|
||||||
// claim.
|
|
||||||
_, pokOpenID := claims.MapClaims[iamPolicyClaimNameOpenID()]
|
|
||||||
_, pokSA := claims.MapClaims[iamPolicyClaimNameSA()]
|
|
||||||
if !pokOpenID && !pokSA {
|
|
||||||
return nil, errAuthentication
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims.Map(), nil
|
return claims.Map(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch claims in the security token returned by the client.
|
||||||
|
func getClaimsFromToken(token string) (map[string]interface{}, error) {
|
||||||
|
return getClaimsFromTokenWithSecret(token, globalActiveCred.SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch claims in the security token returned by the client and validate the token.
|
// Fetch claims in the security token returned by the client and validate the token.
|
||||||
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]interface{}, APIErrorCode) {
|
||||||
token := getSessionToken(r)
|
token := getSessionToken(r)
|
||||||
if token != "" && cred.AccessKey == "" {
|
if token != "" && cred.AccessKey == "" {
|
||||||
|
// x-amz-security-token is not allowed for anonymous access.
|
||||||
return nil, ErrNoAccessKey
|
return nil, ErrNoAccessKey
|
||||||
}
|
}
|
||||||
if cred.IsServiceAccount() && token == "" {
|
|
||||||
token = cred.SessionToken
|
if token == "" && cred.IsTemp() {
|
||||||
}
|
// Temporary credentials should always have x-amz-security-token
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
claims, err := getClaimsFromToken(token)
|
|
||||||
if err != nil {
|
if token != "" && !cred.IsTemp() {
|
||||||
return nil, toAPIErrorCode(r.Context(), err)
|
// x-amz-security-token should not present for static credentials.
|
||||||
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
return claims, ErrNone
|
|
||||||
|
if cred.IsTemp() && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
|
||||||
|
// validate token for temporary credentials only.
|
||||||
|
return nil, ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := globalActiveCred.SecretKey
|
||||||
|
if cred.IsServiceAccount() {
|
||||||
|
token = cred.SessionToken
|
||||||
|
secret = cred.SecretKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
claims, err := getClaimsFromTokenWithSecret(token, secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, toAPIErrorCode(r.Context(), err)
|
||||||
|
}
|
||||||
|
return claims, ErrNone
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := xjwt.NewMapClaims()
|
||||||
|
return claims.Map(), ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check request auth type verifies the incoming http request
|
// Check request auth type verifies the incoming http request
|
||||||
@@ -299,7 +313,7 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
|
|||||||
}
|
}
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeSigned, authTypePresigned:
|
case authTypeSigned, authTypePresigned:
|
||||||
region := globalServerRegion
|
region := globalSite.Region
|
||||||
switch action {
|
switch action {
|
||||||
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
case policy.GetBucketLocationAction, policy.ListAllMyBucketsAction:
|
||||||
region = ""
|
region = ""
|
||||||
@@ -486,11 +500,49 @@ func isSupportedS3AuthType(aType authType) bool {
|
|||||||
func setAuthHandler(h http.Handler) http.Handler {
|
func setAuthHandler(h http.Handler) http.Handler {
|
||||||
// handler for validating incoming authorization headers.
|
// handler for validating incoming authorization headers.
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tc, ok := r.Context().Value(contextTraceReqKey).(*traceCtxt)
|
||||||
|
|
||||||
aType := getRequestAuthType(r)
|
aType := getRequestAuthType(r)
|
||||||
|
if aType == authTypeSigned || aType == authTypeSignedV2 || aType == authTypeStreamingSigned {
|
||||||
|
// Verify if date headers are set, if not reject the request
|
||||||
|
amzDate, errCode := parseAmzDateHeader(r)
|
||||||
|
if errCode != ErrNone {
|
||||||
|
if ok {
|
||||||
|
tc.funcName = "handler.Auth"
|
||||||
|
tc.responseRecorder.LogErrBody = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// All our internal APIs are sensitive towards Date
|
||||||
|
// header, for all requests where Date header is not
|
||||||
|
// present we will reject such clients.
|
||||||
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(errCode), r.URL)
|
||||||
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Verify if the request date header is shifted by less than globalMaxSkewTime parameter in the past
|
||||||
|
// or in the future, reject request otherwise.
|
||||||
|
curTime := UTCNow()
|
||||||
|
if curTime.Sub(amzDate) > globalMaxSkewTime || amzDate.Sub(curTime) > globalMaxSkewTime {
|
||||||
|
if ok {
|
||||||
|
tc.funcName = "handler.Auth"
|
||||||
|
tc.responseRecorder.LogErrBody = true
|
||||||
|
}
|
||||||
|
|
||||||
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrRequestTimeTooSkewed), r.URL)
|
||||||
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsTime, 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
|
if isSupportedS3AuthType(aType) || aType == authTypeJWT || aType == authTypeSTS {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
tc.funcName = "handler.Auth"
|
||||||
|
tc.responseRecorder.LogErrBody = true
|
||||||
|
}
|
||||||
|
|
||||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL)
|
||||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
|
atomic.AddUint64(&globalHTTPStats.rejectedRequestsAuth, 1)
|
||||||
})
|
})
|
||||||
@@ -509,7 +561,7 @@ func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool,
|
|||||||
}
|
}
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypePresigned, authTypeSigned:
|
case authTypePresigned, authTypeSigned:
|
||||||
region := globalServerRegion
|
region := globalSite.Region
|
||||||
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
|
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
|
||||||
return cred, owner, s3Err
|
return cred, owner, s3Err
|
||||||
}
|
}
|
||||||
@@ -576,7 +628,7 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
|
|||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||||
region := globalServerRegion
|
region := globalSite.Region
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
|
|||||||
+21
-12
@@ -32,6 +32,12 @@ import (
|
|||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type nullReader struct{}
|
||||||
|
|
||||||
|
func (r *nullReader) Read(b []byte) (int, error) {
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Test get request auth type.
|
// Test get request auth type.
|
||||||
func TestGetRequestAuthType(t *testing.T) {
|
func TestGetRequestAuthType(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
@@ -341,7 +347,8 @@ func mustNewSignedEmptyMD5Request(method string, urlStr string, contentLength in
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64,
|
func mustNewSignedBadMD5Request(method string, urlStr string, contentLength int64,
|
||||||
body io.ReadSeeker, t *testing.T) *http.Request {
|
body io.ReadSeeker, t *testing.T,
|
||||||
|
) *http.Request {
|
||||||
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
req := mustNewRequest(method, urlStr, contentLength, body, t)
|
||||||
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
req.Header.Set("Content-Md5", "YWFhYWFhYWFhYWFhYWFhCg==")
|
||||||
cred := globalActiveCred
|
cred := globalActiveCred
|
||||||
@@ -362,11 +369,14 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
t.Fatalf("unable initialize config file, %s", err)
|
t.Fatalf("unable initialize config file, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newAllSubsystems()
|
initAllSubsystems()
|
||||||
|
|
||||||
initAllSubsystems(context.Background(), objLayer)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
globalIAMSys.InitStore(objLayer)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -392,10 +402,9 @@ func TestIsReqAuthenticated(t *testing.T) {
|
|||||||
{mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
{mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000", 0, nil, t), ErrNone},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
// Validates all testcases.
|
// Validates all testcases.
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
s3Error := isReqAuthenticated(ctx, testCase.req, globalServerRegion, serviceS3)
|
s3Error := isReqAuthenticated(ctx, testCase.req, globalSite.Region, serviceS3)
|
||||||
if s3Error != testCase.s3Error {
|
if s3Error != testCase.s3Error {
|
||||||
if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(ctx, err) != testCase.s3Error {
|
if _, err := ioutil.ReadAll(testCase.req.Body); toAPIErrorCode(ctx, err) != testCase.s3Error {
|
||||||
t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %s)", i, testCase.s3Error, s3Error, toAPIError(ctx, err).Code)
|
t.Fatalf("Test %d: Unexpected S3 error: want %d - got %d (got after reading request %s)", i, testCase.s3Error, s3Error, toAPIError(ctx, err).Code)
|
||||||
@@ -433,15 +442,15 @@ func TestCheckAdminRequestAuthType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, iampolicy.AllAdminActions, globalServerRegion); s3Error != testCase.ErrCode {
|
if _, s3Error := checkAdminRequestAuth(ctx, testCase.Request, iampolicy.AllAdminActions, globalSite.Region); s3Error != testCase.ErrCode {
|
||||||
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
|
t.Errorf("Test %d: Unexpected s3error returned wanted %d, got %d", i, testCase.ErrCode, s3Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateAdminSignature(t *testing.T) {
|
func TestValidateAdminSignature(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
ctx := context.Background()
|
defer cancel()
|
||||||
|
|
||||||
objLayer, fsDir, err := prepareFS()
|
objLayer, fsDir, err := prepareFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,11 +462,11 @@ func TestValidateAdminSignature(t *testing.T) {
|
|||||||
t.Fatalf("unable initialize config file, %s", err)
|
t.Fatalf("unable initialize config file, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newAllSubsystems()
|
initAllSubsystems()
|
||||||
|
|
||||||
initAllSubsystems(context.Background(), objLayer)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
globalIAMSys.InitStore(objLayer)
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("admin", "mypassword")
|
creds, err := auth.CreateCredentials("admin", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+33
-48
@@ -19,10 +19,10 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"runtime"
|
||||||
|
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/pubsub"
|
||||||
)
|
)
|
||||||
|
|
||||||
// healTask represents what to heal along with options
|
// healTask represents what to heal along with options
|
||||||
@@ -35,7 +35,7 @@ type healTask struct {
|
|||||||
versionID string
|
versionID string
|
||||||
opts madmin.HealOpts
|
opts madmin.HealOpts
|
||||||
// Healing response will be sent here
|
// Healing response will be sent here
|
||||||
responseCh chan healResult
|
respCh chan healResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// healResult represents a healing result with a possible error
|
// healResult represents a healing result with a possible error
|
||||||
@@ -46,54 +46,37 @@ type healResult struct {
|
|||||||
|
|
||||||
// healRoutine receives heal tasks, to heal buckets, objects and format.json
|
// healRoutine receives heal tasks, to heal buckets, objects and format.json
|
||||||
type healRoutine struct {
|
type healRoutine struct {
|
||||||
tasks chan healTask
|
tasks chan healTask
|
||||||
doneCh chan struct{}
|
workers int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new task in the tasks queue
|
func activeListeners() int {
|
||||||
func (h *healRoutine) queueHealTask(task healTask) {
|
|
||||||
h.tasks <- task
|
|
||||||
}
|
|
||||||
|
|
||||||
func waitForLowHTTPReq(maxIO int, maxWait time.Duration) {
|
|
||||||
// No need to wait run at full speed.
|
|
||||||
if maxIO <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// At max 10 attempts to wait with 100 millisecond interval before proceeding
|
|
||||||
waitTick := 100 * time.Millisecond
|
|
||||||
|
|
||||||
// Bucket notification and http trace are not costly, it is okay to ignore them
|
// Bucket notification and http trace are not costly, it is okay to ignore them
|
||||||
// while counting the number of concurrent connections
|
// while counting the number of concurrent connections
|
||||||
maxIOFn := func() int {
|
return int(globalHTTPListen.NumSubscribers(pubsub.MaskAll)) + int(globalTrace.NumSubscribers(pubsub.MaskAll))
|
||||||
return maxIO + int(globalHTTPListen.NumSubscribers()) + int(globalTrace.NumSubscribers())
|
}
|
||||||
|
|
||||||
|
func waitForLowHTTPReq() {
|
||||||
|
var currentIO func() int
|
||||||
|
if httpServer := newHTTPServerFn(); httpServer != nil {
|
||||||
|
currentIO = httpServer.GetRequestCount
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpMaxWait := maxWait
|
globalHealConfig.Wait(currentIO, activeListeners)
|
||||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
}
|
||||||
// Any requests in progress, delay the heal.
|
|
||||||
for httpServer.GetRequestCount() >= maxIOFn() {
|
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||||
if tmpMaxWait > 0 {
|
// Run the background healer
|
||||||
if tmpMaxWait < waitTick {
|
globalBackgroundHealRoutine = newHealRoutine()
|
||||||
time.Sleep(tmpMaxWait)
|
for i := 0; i < globalBackgroundHealRoutine.workers; i++ {
|
||||||
} else {
|
go globalBackgroundHealRoutine.AddWorker(ctx, objAPI)
|
||||||
time.Sleep(waitTick)
|
|
||||||
}
|
|
||||||
tmpMaxWait = tmpMaxWait - waitTick
|
|
||||||
}
|
|
||||||
if tmpMaxWait <= 0 {
|
|
||||||
if intDataUpdateTracker.debug {
|
|
||||||
logger.Info("waitForLowHTTPReq: waited max %s, resuming", maxWait)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalBackgroundHealState.LaunchNewHealSequence(newBgHealSequence(), objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for heal requests and process them
|
// Wait for heal requests and process them
|
||||||
func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
func (h *healRoutine) AddWorker(ctx context.Context, objAPI ObjectLayer) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case task, ok := <-h.tasks:
|
case task, ok := <-h.tasks:
|
||||||
@@ -105,6 +88,7 @@ func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
var err error
|
var err error
|
||||||
switch task.bucket {
|
switch task.bucket {
|
||||||
case nopHeal:
|
case nopHeal:
|
||||||
|
task.respCh <- healResult{err: errSkipFile}
|
||||||
continue
|
continue
|
||||||
case SlashSeparator:
|
case SlashSeparator:
|
||||||
res, err = healDiskFormat(ctx, objAPI, task.opts)
|
res, err = healDiskFormat(ctx, objAPI, task.opts)
|
||||||
@@ -115,10 +99,8 @@ func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
res, err = objAPI.HealObject(ctx, task.bucket, task.object, task.versionID, task.opts)
|
res, err = objAPI.HealObject(ctx, task.bucket, task.object, task.versionID, task.opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
task.responseCh <- healResult{result: res, err: err}
|
|
||||||
|
|
||||||
case <-h.doneCh:
|
task.respCh <- healResult{result: res, err: err}
|
||||||
return
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -126,11 +108,14 @@ func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHealRoutine() *healRoutine {
|
func newHealRoutine() *healRoutine {
|
||||||
return &healRoutine{
|
workers := runtime.GOMAXPROCS(0) / 2
|
||||||
tasks: make(chan healTask),
|
if workers == 0 {
|
||||||
doneCh: make(chan struct{}),
|
workers = 4
|
||||||
|
}
|
||||||
|
return &healRoutine{
|
||||||
|
tasks: make(chan healTask),
|
||||||
|
workers: workers,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// healDiskFormat - heals format.json, return value indicates if a
|
// healDiskFormat - heals format.json, return value indicates if a
|
||||||
|
|||||||
+140
-175
@@ -18,23 +18,20 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio/internal/color"
|
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/console"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -166,7 +163,11 @@ func (h *healingTracker) save(ctx context.Context) error {
|
|||||||
func (h *healingTracker) delete(ctx context.Context) error {
|
func (h *healingTracker) delete(ctx context.Context) error {
|
||||||
return h.disk.Delete(ctx, minioMetaBucket,
|
return h.disk.Delete(ctx, minioMetaBucket,
|
||||||
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
|
pathJoin(bucketMetaPrefix, slashSeparator, healingTrackerFilename),
|
||||||
false)
|
DeleteOptions{
|
||||||
|
Recursive: false,
|
||||||
|
Force: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healingTracker) isHealed(bucket string) bool {
|
func (h *healingTracker) isHealed(bucket string) bool {
|
||||||
@@ -258,68 +259,132 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
|||||||
|
|
||||||
initBackgroundHealing(ctx, objAPI) // start quick background healing
|
initBackgroundHealing(ctx, objAPI) // start quick background healing
|
||||||
|
|
||||||
bgSeq := mustGetHealSequence(ctx)
|
|
||||||
|
|
||||||
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
||||||
|
|
||||||
if drivesToHeal := globalBackgroundHealState.healDriveCount(); drivesToHeal > 0 {
|
go monitorLocalDisksAndHeal(ctx, z)
|
||||||
logger.Info(fmt.Sprintf("Found drives to heal %d, waiting until %s to heal the content...",
|
|
||||||
drivesToHeal, defaultMonitorNewDiskInterval))
|
|
||||||
|
|
||||||
// Heal any disk format and metadata early, if possible.
|
|
||||||
// Start with format healing
|
|
||||||
if err := bgSeq.healDiskFormat(); err != nil {
|
|
||||||
if newObjectLayerFn() != nil {
|
|
||||||
// log only in situations, when object layer
|
|
||||||
// has fully initialized.
|
|
||||||
logger.LogIf(bgSeq.ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bgSeq.healDiskMeta(objAPI); err != nil {
|
|
||||||
if newObjectLayerFn() != nil {
|
|
||||||
// log only in situations, when object layer
|
|
||||||
// has fully initialized.
|
|
||||||
logger.LogIf(bgSeq.ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go monitorLocalDisksAndHeal(ctx, z, bgSeq)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||||
for _, ep := range globalEndpoints {
|
for _, disk := range globalLocalDrives {
|
||||||
for _, endpoint := range ep.Endpoints {
|
_, err := disk.GetDiskID()
|
||||||
if !endpoint.IsLocal {
|
if errors.Is(err, errUnformattedDisk) {
|
||||||
continue
|
disksToHeal = append(disksToHeal, disk.Endpoint())
|
||||||
}
|
continue
|
||||||
// Try to connect to the current endpoint
|
}
|
||||||
// and reformat if the current disk is not formatted
|
if disk.Healing() != nil {
|
||||||
disk, _, err := connectEndpoint(endpoint)
|
disksToHeal = append(disksToHeal, disk.Endpoint())
|
||||||
if errors.Is(err, errUnformattedDisk) {
|
|
||||||
disksToHeal = append(disksToHeal, endpoint)
|
|
||||||
} else if err == nil && disk != nil && disk.Healing() != nil {
|
|
||||||
disksToHeal = append(disksToHeal, disk.Endpoint())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(disksToHeal) == globalEndpoints.NEndpoints() {
|
||||||
|
// When all disks == all command line endpoints
|
||||||
|
// this is a fresh setup, no need to trigger healing.
|
||||||
|
return Endpoints{}
|
||||||
|
}
|
||||||
return disksToHeal
|
return disksToHeal
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
var newDiskHealingTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||||
// Run the background healer
|
|
||||||
globalBackgroundHealRoutine = newHealRoutine()
|
|
||||||
go globalBackgroundHealRoutine.run(ctx, objAPI)
|
|
||||||
|
|
||||||
globalBackgroundHealState.LaunchNewHealSequence(newBgHealSequence(), objAPI)
|
func healFreshDisk(ctx context.Context, z *erasureServerPools, endpoint Endpoint) error {
|
||||||
|
logger.Info(fmt.Sprintf("Proceeding to heal '%s' - 'mc admin heal alias/ --verbose' to check the status.", endpoint))
|
||||||
|
|
||||||
|
disk, format, err := connectEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error: %w, %s", err, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
||||||
|
if poolIdx < 0 {
|
||||||
|
return fmt.Errorf("unexpected pool index (%d) found in %s", poolIdx, disk.Endpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the set index where the current endpoint belongs
|
||||||
|
z.serverPools[poolIdx].erasureDisksMu.RLock()
|
||||||
|
setIdx, _, err := findDiskIndex(z.serverPools[poolIdx].format, format)
|
||||||
|
z.serverPools[poolIdx].erasureDisksMu.RUnlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if setIdx < 0 {
|
||||||
|
return fmt.Errorf("unexpected set index (%d) found in %s", setIdx, disk.Endpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent parallel erasure set healing
|
||||||
|
locker := z.NewNSLock(minioMetaBucket, fmt.Sprintf("new-disk-healing/%s/%d/%d", endpoint, poolIdx, setIdx))
|
||||||
|
lkctx, err := locker.GetLock(ctx, newDiskHealingTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer locker.Unlock(lkctx.Cancel)
|
||||||
|
|
||||||
|
buckets, _ := z.ListBuckets(ctx)
|
||||||
|
|
||||||
|
// Buckets data are dispersed in multiple zones/sets, make
|
||||||
|
// sure to heal all bucket metadata configuration.
|
||||||
|
buckets = append(buckets, BucketInfo{
|
||||||
|
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
||||||
|
}, BucketInfo{
|
||||||
|
Name: pathJoin(minioMetaBucket, bucketMetaPrefix),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Heal latest buckets first.
|
||||||
|
sort.Slice(buckets, func(i, j int) bool {
|
||||||
|
a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
|
||||||
|
if a != b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return buckets[i].Created.After(buckets[j].Created)
|
||||||
|
})
|
||||||
|
|
||||||
|
if serverDebugLog {
|
||||||
|
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(poolIdx+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load healing tracker in this disk
|
||||||
|
tracker, err := loadHealingTracker(ctx, disk)
|
||||||
|
if err != nil {
|
||||||
|
// So someone changed the drives underneath, healing tracker missing.
|
||||||
|
logger.LogIf(ctx, fmt.Errorf("Healing tracker missing on '%s', disk was swapped again on %s pool: %w",
|
||||||
|
disk, humanize.Ordinal(poolIdx+1), err))
|
||||||
|
tracker = newHealingTracker(disk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bucket totals
|
||||||
|
cache := dataUsageCache{}
|
||||||
|
if err := cache.load(ctx, z.serverPools[poolIdx].sets[setIdx], dataUsageCacheName); err == nil {
|
||||||
|
dataUsageInfo := cache.dui(dataUsageRoot, nil)
|
||||||
|
tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
|
||||||
|
tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
|
||||||
|
tracker.setQueuedBuckets(buckets)
|
||||||
|
if err := tracker.save(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start or resume healing of this erasure set
|
||||||
|
err = z.serverPools[poolIdx].sets[setIdx].healErasureSet(ctx, tracker.QueuedBuckets, tracker)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Healing disk '%s' is complete (healed: %d, failed: %d).", disk, tracker.ItemsHealed, tracker.ItemsFailed)
|
||||||
|
|
||||||
|
if serverDebugLog {
|
||||||
|
tracker.printTo(os.Stdout)
|
||||||
|
logger.Info("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogIf(ctx, tracker.delete(ctx))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
||||||
// 1. Only the concerned erasure set will be listed and healed
|
// 1. Only the concerned erasure set will be listed and healed
|
||||||
// 2. Only the node hosting the disk is responsible to perform the heal
|
// 2. Only the node hosting the disk is responsible to perform the heal
|
||||||
func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq *healSequence) {
|
func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools) {
|
||||||
// Perform automatic disk healing when a disk is replaced locally.
|
// Perform automatic disk healing when a disk is replaced locally.
|
||||||
diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval)
|
diskCheckTimer := time.NewTimer(defaultMonitorNewDiskInterval)
|
||||||
defer diskCheckTimer.Stop()
|
defer diskCheckTimer.Stop()
|
||||||
@@ -329,138 +394,38 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-diskCheckTimer.C:
|
case <-diskCheckTimer.C:
|
||||||
// Reset to next interval.
|
|
||||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
|
||||||
|
|
||||||
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
|
|
||||||
|
|
||||||
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
||||||
if len(healDisks) > 0 {
|
if len(healDisks) == 0 {
|
||||||
// Reformat disks
|
// Reset for next interval.
|
||||||
bgSeq.sourceCh <- healSource{bucket: SlashSeparator}
|
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||||
|
break
|
||||||
// Ensure that reformatting disks is finished
|
|
||||||
bgSeq.sourceCh <- healSource{bucket: nopHeal}
|
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("Found drives to heal %d, proceeding to heal content...",
|
|
||||||
len(healDisks)))
|
|
||||||
|
|
||||||
erasureSetInPoolDisksToHeal = make([]map[int][]StorageAPI, len(z.serverPools))
|
|
||||||
for i := range z.serverPools {
|
|
||||||
erasureSetInPoolDisksToHeal[i] = map[int][]StorageAPI{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if serverDebugLog && len(healDisks) > 0 {
|
// Reformat disks immediately
|
||||||
console.Debugf(color.Green("healDisk:")+" disk check timer fired, attempting to heal %d drives\n", len(healDisks))
|
_, err := z.HealFormat(context.Background(), false)
|
||||||
|
if err != nil && !errors.Is(err, errNoHealRequired) {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
// Reset for next interval.
|
||||||
|
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// heal only if new disks found.
|
for _, disk := range healDisks {
|
||||||
for _, endpoint := range healDisks {
|
go func(disk Endpoint) {
|
||||||
disk, format, err := connectEndpoint(endpoint)
|
globalBackgroundHealState.markDiskForHealing(disk)
|
||||||
if err != nil {
|
err := healFreshDisk(ctx, z, disk)
|
||||||
printEndpointError(endpoint, err, true)
|
if err != nil {
|
||||||
continue
|
printEndpointError(disk, err, false)
|
||||||
}
|
return
|
||||||
|
|
||||||
poolIdx := globalEndpoints.GetLocalPoolIdx(disk.Endpoint())
|
|
||||||
if poolIdx < 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the set index where the current endpoint belongs
|
|
||||||
z.serverPools[poolIdx].erasureDisksMu.RLock()
|
|
||||||
// Protect reading reference format.
|
|
||||||
setIndex, _, err := findDiskIndex(z.serverPools[poolIdx].format, format)
|
|
||||||
z.serverPools[poolIdx].erasureDisksMu.RUnlock()
|
|
||||||
if err != nil {
|
|
||||||
printEndpointError(endpoint, err, false)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
erasureSetInPoolDisksToHeal[poolIdx][setIndex] = append(erasureSetInPoolDisksToHeal[poolIdx][setIndex], disk)
|
|
||||||
}
|
|
||||||
|
|
||||||
buckets, _ := z.ListBuckets(ctx)
|
|
||||||
|
|
||||||
buckets = append(buckets, BucketInfo{
|
|
||||||
Name: pathJoin(minioMetaBucket, minioConfigPrefix),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Buckets data are dispersed in multiple zones/sets, make
|
|
||||||
// sure to heal all bucket metadata configuration.
|
|
||||||
buckets = append(buckets, []BucketInfo{
|
|
||||||
{Name: pathJoin(minioMetaBucket, bucketMetaPrefix)},
|
|
||||||
}...)
|
|
||||||
|
|
||||||
// Heal latest buckets first.
|
|
||||||
sort.Slice(buckets, func(i, j int) bool {
|
|
||||||
a, b := strings.HasPrefix(buckets[i].Name, minioMetaBucket), strings.HasPrefix(buckets[j].Name, minioMetaBucket)
|
|
||||||
if a != b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return buckets[i].Created.After(buckets[j].Created)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO(klauspost): This will block until all heals are done,
|
|
||||||
// in the future this should be able to start healing other sets at once.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i, setMap := range erasureSetInPoolDisksToHeal {
|
|
||||||
i := i
|
|
||||||
for setIndex, disks := range setMap {
|
|
||||||
if len(disks) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
wg.Add(1)
|
// Only upon success pop the healed disk.
|
||||||
go func(setIndex int, disks []StorageAPI) {
|
globalBackgroundHealState.popHealLocalDisks(disk)
|
||||||
defer wg.Done()
|
}(disk)
|
||||||
for _, disk := range disks {
|
|
||||||
logger.Info("Healing disk '%v' on %s pool", disk, humanize.Ordinal(i+1))
|
|
||||||
|
|
||||||
// So someone changed the drives underneath, healing tracker missing.
|
|
||||||
tracker, err := loadHealingTracker(ctx, disk)
|
|
||||||
if err != nil {
|
|
||||||
logger.Info("Healing tracker missing on '%s', disk was swapped again on %s pool", disk, humanize.Ordinal(i+1))
|
|
||||||
tracker = newHealingTracker(disk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load bucket totals
|
|
||||||
cache := dataUsageCache{}
|
|
||||||
if err := cache.load(ctx, z.serverPools[i].sets[setIndex], dataUsageCacheName); err == nil {
|
|
||||||
dataUsageInfo := cache.dui(dataUsageRoot, nil)
|
|
||||||
tracker.ObjectsTotalCount = dataUsageInfo.ObjectsTotalCount
|
|
||||||
tracker.ObjectsTotalSize = dataUsageInfo.ObjectsTotalSize
|
|
||||||
}
|
|
||||||
|
|
||||||
tracker.PoolIndex, tracker.SetIndex, tracker.DiskIndex = disk.GetDiskLoc()
|
|
||||||
tracker.setQueuedBuckets(buckets)
|
|
||||||
if err := tracker.save(ctx); err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
// Unable to write healing tracker, permission denied or some
|
|
||||||
// other unexpected error occurred. Proceed to look for new
|
|
||||||
// disks to be healed again, we cannot proceed further.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = z.serverPools[i].sets[setIndex].healErasureSet(ctx, buckets, tracker)
|
|
||||||
if err != nil {
|
|
||||||
logger.LogIf(ctx, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Healing disk '%s' on %s pool complete", disk, humanize.Ordinal(i+1))
|
|
||||||
var buf bytes.Buffer
|
|
||||||
tracker.printTo(&buf)
|
|
||||||
logger.Info("Summary:\n%s", buf.String())
|
|
||||||
logger.LogIf(ctx, tracker.delete(ctx))
|
|
||||||
|
|
||||||
// Only upon success pop the healed disk.
|
|
||||||
globalBackgroundHealState.popHealLocalDisks(disk.Endpoint())
|
|
||||||
}
|
|
||||||
}(setIndex, disks)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
|
// Reset for next interval.
|
||||||
|
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
|
|||||||
b.closeWithErr(err)
|
b.closeWithErr(err)
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
if n != len(p) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
b.closeWithErr(err)
|
||||||
|
}
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,10 +152,10 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
|||||||
streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
|
streamOffset := (offset/b.shardSize)*int64(b.h.Size()) + offset
|
||||||
if len(b.data) == 0 && b.tillOffset != streamOffset {
|
if len(b.data) == 0 && b.tillOffset != streamOffset {
|
||||||
b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
|
b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
|
||||||
if err != nil {
|
if err != nil && err != errDiskNotFound {
|
||||||
logger.LogIf(GlobalContext,
|
logger.LogIf(GlobalContext,
|
||||||
fmt.Errorf("Error(%w) reading erasure shards at (%s: %s/%s), will attempt to reconstruct if we have quorum",
|
fmt.Errorf("Reading erasure shards at (%s: %s/%s) returned '%w', will attempt to reconstruct if we have quorum",
|
||||||
err, b.disk, b.volume, b.filePath))
|
b.disk, b.volume, b.filePath, err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.rc = io.NewSectionReader(bytes.NewReader(b.data), streamOffset, b.tillOffset-streamOffset)
|
b.rc = io.NewSectionReader(bytes.NewReader(b.data), streamOffset, b.tillOffset-streamOffset)
|
||||||
|
|||||||
+15
-9
@@ -19,7 +19,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -27,8 +26,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/minio/highwayhash"
|
"github.com/minio/highwayhash"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/hash/sha256"
|
||||||
"golang.org/x/crypto/blake2b"
|
"golang.org/x/crypto/blake2b"
|
||||||
|
|
||||||
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
|
// magic HH-256 key as HH-256 hash of the first 100 decimals of π as utf-8 string with a zero key.
|
||||||
@@ -117,8 +119,10 @@ func newBitrotReader(disk StorageAPI, data []byte, bucket string, filePath strin
|
|||||||
// Close all the readers.
|
// Close all the readers.
|
||||||
func closeBitrotReaders(rs []io.ReaderAt) {
|
func closeBitrotReaders(rs []io.ReaderAt) {
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
if br, ok := r.(io.Closer); ok {
|
if r != nil {
|
||||||
br.Close()
|
if br, ok := r.(io.Closer); ok {
|
||||||
|
br.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,8 +130,10 @@ func closeBitrotReaders(rs []io.ReaderAt) {
|
|||||||
// Close all the writers.
|
// Close all the writers.
|
||||||
func closeBitrotWriters(ws []io.Writer) {
|
func closeBitrotWriters(ws []io.Writer) {
|
||||||
for _, w := range ws {
|
for _, w := range ws {
|
||||||
if bw, ok := w.(io.Closer); ok {
|
if w != nil {
|
||||||
bw.Close()
|
if bw, ok := w.(io.Closer); ok {
|
||||||
|
bw.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,8 +178,8 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
|
|||||||
return errFileCorrupt
|
return errFileCorrupt
|
||||||
}
|
}
|
||||||
|
|
||||||
bufp := xlPoolSmall.Get().(*[]byte)
|
bufp := xioutil.ODirectPoolSmall.Get().(*[]byte)
|
||||||
defer xlPoolSmall.Put(bufp)
|
defer xioutil.ODirectPoolSmall.Put(bufp)
|
||||||
|
|
||||||
for left > 0 {
|
for left > 0 {
|
||||||
// Read expected hash...
|
// Read expected hash...
|
||||||
@@ -210,7 +216,7 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
|
|||||||
// bitrotSelfTest tries to catch any issue in the bitrot implementation
|
// bitrotSelfTest tries to catch any issue in the bitrot implementation
|
||||||
// early instead of silently corrupting data.
|
// early instead of silently corrupting data.
|
||||||
func bitrotSelfTest() {
|
func bitrotSelfTest() {
|
||||||
var checksums = map[BitrotAlgorithm]string{
|
checksums := map[BitrotAlgorithm]string{
|
||||||
SHA256: "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004",
|
SHA256: "a7677ff19e0182e4d52e3a3db727804abc82a5818749336369552e54b838b004",
|
||||||
BLAKE2b512: "e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
|
BLAKE2b512: "e519b7d84b1c3c917985f544773a35cf265dcab10948be3550320d156bab612124a5ae2ae5a8c73c0eea360f68b0e28136f26e858756dbfe7375a7389f26c669",
|
||||||
HighwayHash256: "39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
|
HighwayHash256: "39c0407ed3f01b18d22c85db4aeff11e060ca5f43131b0126731ca197cd42313",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ import (
|
|||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/rest"
|
"github.com/minio/minio/internal/rest"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,8 +54,8 @@ type bootstrapRESTServer struct{}
|
|||||||
// ServerSystemConfig - captures information about server configuration.
|
// ServerSystemConfig - captures information about server configuration.
|
||||||
type ServerSystemConfig struct {
|
type ServerSystemConfig struct {
|
||||||
MinioPlatform string
|
MinioPlatform string
|
||||||
MinioRuntime string
|
|
||||||
MinioEndpoints EndpointServerPools
|
MinioEndpoints EndpointServerPools
|
||||||
|
MinioEnv map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff - returns error on first difference found in two configs.
|
// Diff - returns error on first difference found in two configs.
|
||||||
@@ -82,15 +84,50 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
|||||||
s2.MinioEndpoints[i].Endpoints[j])
|
s2.MinioEndpoints[i].Endpoints[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(s1.MinioEnv, s2.MinioEnv) {
|
||||||
|
var missing []string
|
||||||
|
var mismatching []string
|
||||||
|
for k, v := range s1.MinioEnv {
|
||||||
|
ev, ok := s2.MinioEnv[k]
|
||||||
|
if !ok {
|
||||||
|
missing = append(missing, k)
|
||||||
|
} else if v != ev {
|
||||||
|
mismatching = append(mismatching, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mismatching) > 0 {
|
||||||
|
return fmt.Errorf(`Expected same MINIO_ environment variables and values across all servers: Missing environment values: %s / Mismatch environment values: %s`, missing, mismatching)
|
||||||
|
}
|
||||||
|
return fmt.Errorf(`Expected same MINIO_ environment variables and values across all servers: Missing environment values: %s`, missing)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var skipEnvs = map[string]struct{}{
|
||||||
|
"MINIO_OPTS": {},
|
||||||
|
"MINIO_CERT_PASSWD": {},
|
||||||
|
"MINIO_SERVER_DEBUG": {},
|
||||||
|
"MINIO_DSYNC_TRACE": {},
|
||||||
|
}
|
||||||
|
|
||||||
func getServerSystemCfg() ServerSystemConfig {
|
func getServerSystemCfg() ServerSystemConfig {
|
||||||
|
envs := env.List("MINIO_")
|
||||||
|
envValues := make(map[string]string, len(envs))
|
||||||
|
for _, envK := range envs {
|
||||||
|
// skip certain environment variables as part
|
||||||
|
// of the whitelist and could be configured
|
||||||
|
// differently on each nodes, update skipEnvs()
|
||||||
|
// map if there are such environment values
|
||||||
|
if _, ok := skipEnvs[envK]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
envValues[envK] = env.Get(envK, "")
|
||||||
|
}
|
||||||
return ServerSystemConfig{
|
return ServerSystemConfig{
|
||||||
MinioPlatform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
|
MinioPlatform: fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH),
|
||||||
MinioEndpoints: globalEndpoints,
|
MinioEndpoints: globalEndpoints,
|
||||||
|
MinioEnv: envValues,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +138,6 @@ func (b *bootstrapRESTServer) VerifyHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
ctx := newContext(r, w, "VerifyHandler")
|
ctx := newContext(r, w, "VerifyHandler")
|
||||||
cfg := getServerSystemCfg()
|
cfg := getServerSystemCfg()
|
||||||
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
|
logger.LogIf(ctx, json.NewEncoder(w).Encode(&cfg))
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerBootstrapRESTHandlers - register bootstrap rest router.
|
// registerBootstrapRESTHandlers - register bootstrap rest router.
|
||||||
@@ -169,11 +205,11 @@ func verifyServerSystemConfig(ctx context.Context, endpointServerPools EndpointS
|
|||||||
for onlineServers < len(clnts)/2 {
|
for onlineServers < len(clnts)/2 {
|
||||||
for _, clnt := range clnts {
|
for _, clnt := range clnts {
|
||||||
if err := clnt.Verify(ctx, srcCfg); err != nil {
|
if err := clnt.Verify(ctx, srcCfg); err != nil {
|
||||||
if isNetworkError(err) {
|
if !isNetworkError(err) {
|
||||||
offlineEndpoints = append(offlineEndpoints, clnt.String())
|
logger.LogIf(ctx, fmt.Errorf("%s has incorrect configuration: %w", clnt.String(), err))
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s as has incorrect configuration: %w", clnt.String(), err)
|
offlineEndpoints = append(offlineEndpoints, clnt.String())
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
onlineServers++
|
onlineServers++
|
||||||
}
|
}
|
||||||
@@ -226,7 +262,7 @@ func newBootstrapRESTClient(endpoint Endpoint) *bootstrapRESTClient {
|
|||||||
Path: bootstrapRESTPath,
|
Path: bootstrapRESTPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
restClient := rest.NewClient(serverURL, globalInternodeTransport, newAuthToken)
|
restClient := rest.NewClient(serverURL, globalInternodeTransport, newCachedAuthToken())
|
||||||
restClient.HealthCheckFn = nil
|
restClient.HealthCheckFn = nil
|
||||||
|
|
||||||
return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient}
|
return &bootstrapRESTClient{endpoint: endpoint, restClient: restClient}
|
||||||
|
|||||||
@@ -18,12 +18,17 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/kes"
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
)
|
)
|
||||||
@@ -82,6 +87,19 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
kmsKey := encConfig.KeyID()
|
||||||
|
if kmsKey != "" {
|
||||||
|
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||||
|
_, err := GlobalKMS.GenerateKey(kmsKey, kmsContext)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configData, err := xml.Marshal(encConfig)
|
configData, err := xml.Marshal(encConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,7 +108,23 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the bucket encryption configuration in the object layer
|
// Store the bucket encryption configuration in the object layer
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketSSEConfig, configData); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
SSEConfig: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -126,7 +160,7 @@ func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -170,10 +204,21 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete bucket encryption config from object layer
|
// Delete bucket encryption config from object layer
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketSSEConfig, nil); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketSSEConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeSSEConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
SSEConfig: nil,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
||||||
@@ -33,7 +33,7 @@ func NewBucketSSEConfigSys() *BucketSSEConfigSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get - gets bucket encryption config for the given bucket.
|
// Get - gets bucket encryption config for the given bucket.
|
||||||
func (sys *BucketSSEConfigSys) Get(bucket string) (*bucketsse.BucketSSEConfig, error) {
|
func (sys *BucketSSEConfigSys) Get(bucket string) (*sse.BucketSSEConfig, error) {
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
@@ -43,12 +43,13 @@ func (sys *BucketSSEConfigSys) Get(bucket string) (*bucketsse.BucketSSEConfig, e
|
|||||||
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
|
|
||||||
return globalBucketMetadataSys.GetSSEConfig(bucket)
|
sseCfg, _, err := globalBucketMetadataSys.GetSSEConfig(bucket)
|
||||||
|
return sseCfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
|
// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
|
||||||
func validateBucketSSEConfig(r io.Reader) (*bucketsse.BucketSSEConfig, error) {
|
func validateBucketSSEConfig(r io.Reader) (*sse.BucketSSEConfig, error) {
|
||||||
encConfig, err := bucketsse.ParseBucketSSEConfig(r)
|
encConfig, err := sse.ParseBucketSSEConfig(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+217
-381
@@ -19,8 +19,8 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -32,13 +32,14 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
|
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
@@ -130,8 +131,6 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
// Add/update buckets that are not registered with the DNS
|
// Add/update buckets that are not registered with the DNS
|
||||||
bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
|
bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
|
||||||
g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)).WithConcurrency(50)
|
g := errgroup.WithNErrs(len(bucketsToBeUpdatedSlice)).WithConcurrency(50)
|
||||||
ctx, cancel := g.WithCancelOnError(GlobalContext)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
for index := range bucketsToBeUpdatedSlice {
|
for index := range bucketsToBeUpdatedSlice {
|
||||||
index := index
|
index := index
|
||||||
@@ -140,9 +139,12 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
|||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.WaitErr(); err != nil {
|
ctx := GlobalContext
|
||||||
logger.LogIf(ctx, err)
|
for _, err := range g.Wait() {
|
||||||
return
|
if err != nil {
|
||||||
|
logger.LogIf(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, bucket := range bucketsInConflict.ToSlice() {
|
for _, bucket := range bucketsInConflict.ToSlice() {
|
||||||
@@ -207,7 +209,7 @@ func (api objectAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *
|
|||||||
// Generate response.
|
// Generate response.
|
||||||
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
||||||
// Get current region.
|
// Get current region.
|
||||||
region := globalServerRegion
|
region := globalSite.Region
|
||||||
if region != globalMinioDefaultRegion {
|
if region != globalMinioDefaultRegion {
|
||||||
encodedSuccessResponse = encodeResponse(LocationResponse{
|
encodedSuccessResponse = encodeResponse(LocationResponse{
|
||||||
Location: region,
|
Location: region,
|
||||||
@@ -360,6 +362,18 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
}) {
|
}) {
|
||||||
bucketsInfo[n] = bucketInfo
|
bucketsInfo[n] = bucketInfo
|
||||||
n++
|
n++
|
||||||
|
} else if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: iampolicy.GetBucketLocationAction,
|
||||||
|
BucketName: bucketInfo.Name,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
||||||
|
IsOwner: owner,
|
||||||
|
ObjectName: "",
|
||||||
|
Claims: cred.Claims,
|
||||||
|
}) {
|
||||||
|
bucketsInfo[n] = bucketInfo
|
||||||
|
n++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bucketsInfo = bucketsInfo[:n]
|
bucketsInfo = bucketsInfo[:n]
|
||||||
@@ -411,18 +425,23 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
const maxBodySize = 2 * 100000 * 1024
|
const maxBodySize = 2 * 100000 * 1024
|
||||||
|
|
||||||
// Unmarshal list of keys to be deleted.
|
// Unmarshal list of keys to be deleted.
|
||||||
deleteObjects := &DeleteObjectsRequest{}
|
deleteObjectsReq := &DeleteObjectsRequest{}
|
||||||
if err := xmlDecoder(r.Body, deleteObjects, maxBodySize); err != nil {
|
if err := xmlDecoder(r.Body, deleteObjectsReq, maxBodySize); err != nil {
|
||||||
logger.LogIf(ctx, err, logger.Application)
|
logger.LogIf(ctx, err, logger.Application)
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
objects := make([]ObjectV, len(deleteObjectsReq.Objects))
|
||||||
// Convert object name delete objects if it has `/` in the beginning.
|
// Convert object name delete objects if it has `/` in the beginning.
|
||||||
for i := range deleteObjects.Objects {
|
for i := range deleteObjectsReq.Objects {
|
||||||
deleteObjects.Objects[i].ObjectName = trimLeadingSlash(deleteObjects.Objects[i].ObjectName)
|
deleteObjectsReq.Objects[i].ObjectName = trimLeadingSlash(deleteObjectsReq.Objects[i].ObjectName)
|
||||||
|
objects[i] = deleteObjectsReq.Objects[i].ObjectV
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure to update context to print ObjectNames for multi objects.
|
||||||
|
ctx = updateReqContext(ctx, objects...)
|
||||||
|
|
||||||
// Call checkRequestAuthType to populate ReqInfo.AccessKey before GetBucketInfo()
|
// Call checkRequestAuthType to populate ReqInfo.AccessKey before GetBucketInfo()
|
||||||
// Ignore errors here to preserve the S3 error behavior of GetBucketInfo()
|
// Ignore errors here to preserve the S3 error behavior of GetBucketInfo()
|
||||||
checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "")
|
checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, "")
|
||||||
@@ -439,41 +458,46 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
deleteObjectsFn = api.CacheAPI().DeleteObjects
|
deleteObjectsFn = api.CacheAPI().DeleteObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return Malformed XML as S3 spec if the list of objects is empty
|
// Return Malformed XML as S3 spec if the number of objects is empty
|
||||||
if len(deleteObjects.Objects) == 0 {
|
if len(deleteObjectsReq.Objects) == 0 || len(deleteObjectsReq.Objects) > maxDeleteList {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var objectsToDelete = map[ObjectToDelete]int{}
|
objectsToDelete := map[ObjectToDelete]int{}
|
||||||
getObjectInfoFn := objectAPI.GetObjectInfo
|
getObjectInfoFn := objectAPI.GetObjectInfo
|
||||||
if api.CacheAPI() != nil {
|
if api.CacheAPI() != nil {
|
||||||
getObjectInfoFn = api.CacheAPI().GetObjectInfo
|
getObjectInfoFn = api.CacheAPI().GetObjectInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
hasLockEnabled, replicateSync bool
|
hasLockEnabled bool
|
||||||
goi ObjectInfo
|
dsc ReplicateDecision
|
||||||
gerr error
|
goi ObjectInfo
|
||||||
|
gerr error
|
||||||
)
|
)
|
||||||
replicateDeletes := hasReplicationRules(ctx, bucket, deleteObjects.Objects)
|
replicateDeletes := hasReplicationRules(ctx, bucket, deleteObjectsReq.Objects)
|
||||||
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
|
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled {
|
||||||
hasLockEnabled = true
|
hasLockEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
type deleteResult struct {
|
||||||
suspended := globalBucketVersioningSys.Suspended(bucket)
|
delInfo DeletedObject
|
||||||
|
errInfo DeleteError
|
||||||
|
}
|
||||||
|
|
||||||
dErrs := make([]DeleteError, len(deleteObjects.Objects))
|
deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
|
||||||
oss := make([]*objSweeper, len(deleteObjects.Objects))
|
|
||||||
for index, object := range deleteObjects.Objects {
|
vc, _ := globalBucketVersioningSys.Get(bucket)
|
||||||
|
oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
|
||||||
|
for index, object := range deleteObjectsReq.Objects {
|
||||||
if apiErrCode := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
if apiErrCode := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
||||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||||
dErrs[index] = DeleteError{
|
deleteResults[index].errInfo = DeleteError{
|
||||||
Code: apiErr.Code,
|
Code: apiErr.Code,
|
||||||
Message: apiErr.Description,
|
Message: apiErr.Description,
|
||||||
Key: object.ObjectName,
|
Key: object.ObjectName,
|
||||||
@@ -485,7 +509,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
if _, err := uuid.Parse(object.VersionID); err != nil {
|
if _, err := uuid.Parse(object.VersionID); err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("invalid version-id specified %w", err))
|
logger.LogIf(ctx, fmt.Errorf("invalid version-id specified %w", err))
|
||||||
apiErr := errorCodes.ToAPIErr(ErrNoSuchVersion)
|
apiErr := errorCodes.ToAPIErr(ErrNoSuchVersion)
|
||||||
dErrs[index] = DeleteError{
|
deleteResults[index].errInfo = DeleteError{
|
||||||
Code: apiErr.Code,
|
Code: apiErr.Code,
|
||||||
Message: apiErr.Description,
|
Message: apiErr.Description,
|
||||||
Key: object.ObjectName,
|
Key: object.ObjectName,
|
||||||
@@ -497,8 +521,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
opts := ObjectOptions{
|
opts := ObjectOptions{
|
||||||
VersionID: object.VersionID,
|
VersionID: object.VersionID,
|
||||||
Versioned: versioned,
|
Versioned: vc.PrefixEnabled(object.ObjectName),
|
||||||
VersionSuspended: suspended,
|
VersionSuspended: vc.Suspended(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||||
@@ -509,35 +533,31 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !globalTierConfigMgr.Empty() {
|
if !globalTierConfigMgr.Empty() {
|
||||||
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(versioned, suspended)
|
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(opts.Versioned, opts.VersionSuspended)
|
||||||
oss[index].SetTransitionState(goi.TransitionedObject)
|
oss[index].SetTransitionState(goi.TransitionedObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes {
|
if replicateDeletes {
|
||||||
replicate, repsync := checkReplicateDelete(ctx, bucket, ObjectToDelete{
|
dsc = checkReplicateDelete(ctx, bucket, ObjectToDelete{
|
||||||
ObjectName: object.ObjectName,
|
ObjectV: ObjectV{
|
||||||
VersionID: object.VersionID,
|
ObjectName: object.ObjectName,
|
||||||
}, goi, gerr)
|
VersionID: object.VersionID,
|
||||||
replicateSync = repsync
|
},
|
||||||
if replicate {
|
}, goi, opts, gerr)
|
||||||
if apiErrCode := checkRequestAuthType(ctx, r, policy.ReplicateDeleteAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
if dsc.ReplicateAny() {
|
||||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if object.VersionID != "" {
|
if object.VersionID != "" {
|
||||||
object.VersionPurgeStatus = Pending
|
object.VersionPurgeStatus = Pending
|
||||||
|
object.VersionPurgeStatuses = dsc.PendingStatus()
|
||||||
} else {
|
} else {
|
||||||
object.DeleteMarkerReplicationStatus = string(replication.Pending)
|
object.DeleteMarkerReplicationStatus = dsc.PendingStatus()
|
||||||
}
|
}
|
||||||
|
object.ReplicateDecisionStr = dsc.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if object.VersionID != "" && hasLockEnabled {
|
if object.VersionID != "" && hasLockEnabled {
|
||||||
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
||||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||||
dErrs[index] = DeleteError{
|
deleteResults[index].errInfo = DeleteError{
|
||||||
Code: apiErr.Code,
|
Code: apiErr.Code,
|
||||||
Message: apiErr.Description,
|
Message: apiErr.Description,
|
||||||
Key: object.ObjectName,
|
Key: object.ObjectName,
|
||||||
@@ -563,34 +583,40 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Disable timeouts and cancellation
|
||||||
|
ctx = bgContext(ctx)
|
||||||
|
|
||||||
deleteList := toNames(objectsToDelete)
|
deleteList := toNames(objectsToDelete)
|
||||||
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
|
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
|
||||||
Versioned: versioned,
|
PrefixEnabledFn: vc.PrefixEnabled,
|
||||||
VersionSuspended: suspended,
|
VersionSuspended: vc.Suspended(),
|
||||||
})
|
})
|
||||||
deletedObjects := make([]DeletedObject, len(deleteObjects.Objects))
|
|
||||||
for i := range errs {
|
for i := range errs {
|
||||||
// DeleteMarkerVersionID is not used specifically to avoid
|
// DeleteMarkerVersionID is not used specifically to avoid
|
||||||
// lookup errors, since DeleteMarkerVersionID is only
|
// lookup errors, since DeleteMarkerVersionID is only
|
||||||
// created during DeleteMarker creation when client didn't
|
// created during DeleteMarker creation when client didn't
|
||||||
// specify a versionID.
|
// specify a versionID.
|
||||||
objToDel := ObjectToDelete{
|
objToDel := ObjectToDelete{
|
||||||
ObjectName: dObjects[i].ObjectName,
|
ObjectV: ObjectV{
|
||||||
VersionID: dObjects[i].VersionID,
|
ObjectName: dObjects[i].ObjectName,
|
||||||
VersionPurgeStatus: dObjects[i].VersionPurgeStatus,
|
VersionID: dObjects[i].VersionID,
|
||||||
DeleteMarkerReplicationStatus: dObjects[i].DeleteMarkerReplicationStatus,
|
},
|
||||||
|
VersionPurgeStatus: dObjects[i].VersionPurgeStatus(),
|
||||||
|
VersionPurgeStatuses: dObjects[i].ReplicationState.VersionPurgeStatusInternal,
|
||||||
|
DeleteMarkerReplicationStatus: dObjects[i].ReplicationState.ReplicationStatusInternal,
|
||||||
|
ReplicateDecisionStr: dObjects[i].ReplicationState.ReplicateDecisionStr,
|
||||||
}
|
}
|
||||||
dindex := objectsToDelete[objToDel]
|
dindex := objectsToDelete[objToDel]
|
||||||
if errs[i] == nil || isErrObjectNotFound(errs[i]) || isErrVersionNotFound(errs[i]) {
|
if errs[i] == nil || isErrObjectNotFound(errs[i]) || isErrVersionNotFound(errs[i]) {
|
||||||
if replicateDeletes {
|
if replicateDeletes {
|
||||||
dObjects[i].DeleteMarkerReplicationStatus = deleteList[i].DeleteMarkerReplicationStatus
|
dObjects[i].ReplicationState = deleteList[i].ReplicationState()
|
||||||
dObjects[i].VersionPurgeStatus = deleteList[i].VersionPurgeStatus
|
|
||||||
}
|
}
|
||||||
deletedObjects[dindex] = dObjects[i]
|
deleteResults[dindex].delInfo = dObjects[i]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
apiErr := toAPIError(ctx, errs[i])
|
apiErr := toAPIError(ctx, errs[i])
|
||||||
dErrs[dindex] = DeleteError{
|
deleteResults[dindex].errInfo = DeleteError{
|
||||||
Code: apiErr.Code,
|
Code: apiErr.Code,
|
||||||
Message: apiErr.Description,
|
Message: apiErr.Description,
|
||||||
Key: deleteList[i].ObjectName,
|
Key: deleteList[i].ObjectName,
|
||||||
@@ -598,15 +624,18 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var deleteErrors []DeleteError
|
// Generate response
|
||||||
for _, dErr := range dErrs {
|
deleteErrors := make([]DeleteError, 0, len(deleteObjectsReq.Objects))
|
||||||
if dErr.Code != "" {
|
deletedObjects := make([]DeletedObject, 0, len(deleteObjectsReq.Objects))
|
||||||
deleteErrors = append(deleteErrors, dErr)
|
for _, deleteResult := range deleteResults {
|
||||||
|
if deleteResult.errInfo.Code != "" {
|
||||||
|
deleteErrors = append(deleteErrors, deleteResult.errInfo)
|
||||||
|
} else {
|
||||||
|
deletedObjects = append(deletedObjects, deleteResult.delInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate response
|
response := generateMultiDeleteResponse(deleteObjectsReq.Quiet, deletedObjects, deleteErrors)
|
||||||
response := generateMultiDeleteResponse(deleteObjects.Quiet, deletedObjects, deleteErrors)
|
|
||||||
encodedSuccessResponse := encodeResponse(response)
|
encodedSuccessResponse := encodeResponse(response)
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
@@ -616,22 +645,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes {
|
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
|
||||||
if dobj.DeleteMarkerReplicationStatus == string(replication.Pending) || dobj.VersionPurgeStatus == Pending {
|
dv := DeletedObjectReplicationInfo{
|
||||||
dv := DeletedObjectReplicationInfo{
|
DeletedObject: dobj,
|
||||||
DeletedObject: dobj,
|
Bucket: bucket,
|
||||||
Bucket: bucket,
|
EventType: ReplicateIncomingDelete,
|
||||||
}
|
|
||||||
scheduleReplicationDelete(ctx, dv, objectAPI, replicateSync)
|
|
||||||
}
|
}
|
||||||
}
|
scheduleReplicationDelete(ctx, dv, objectAPI)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify deleted event for objects.
|
|
||||||
for _, dobj := range deletedObjects {
|
|
||||||
if dobj.ObjectName == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eventName := event.ObjectRemovedDelete
|
eventName := event.ObjectRemovedDelete
|
||||||
@@ -684,13 +704,27 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
objectLockEnabled := false
|
objectLockEnabled := false
|
||||||
if vs, found := r.Header[http.CanonicalHeaderKey("x-amz-bucket-object-lock-enabled")]; found {
|
if vs := r.Header.Get(xhttp.AmzObjectLockEnabled); len(vs) > 0 {
|
||||||
v := strings.ToLower(strings.Join(vs, ""))
|
v := strings.ToLower(vs)
|
||||||
if v != "true" && v != "false" {
|
switch v {
|
||||||
|
case "true", "false":
|
||||||
|
objectLockEnabled = v == "true"
|
||||||
|
default:
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forceCreate := false
|
||||||
|
if vs := r.Header.Get(xhttp.MinIOForceCreate); len(vs) > 0 {
|
||||||
|
v := strings.ToLower(vs)
|
||||||
|
switch v {
|
||||||
|
case "true", "false":
|
||||||
|
forceCreate = v == "true"
|
||||||
|
default:
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objectLockEnabled = v == "true"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.CreateBucketAction, bucket, ""); s3Error != ErrNone {
|
||||||
@@ -715,6 +749,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
opts := BucketOptions{
|
opts := BucketOptions{
|
||||||
Location: location,
|
Location: location,
|
||||||
LockEnabled: objectLockEnabled,
|
LockEnabled: objectLockEnabled,
|
||||||
|
ForceCreate: forceCreate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
@@ -730,7 +765,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||||
objectAPI.DeleteBucket(ctx, bucket, false)
|
objectAPI.DeleteBucket(context.Background(), bucket, DeleteBucketOptions{Force: false, NoRecreate: true})
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -772,8 +807,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proceed to creating a bucket.
|
// Proceed to creating a bucket.
|
||||||
err := objectAPI.MakeBucketWithLocation(ctx, bucket, opts)
|
if err := objectAPI.MakeBucketWithLocation(ctx, bucket, opts); err != nil {
|
||||||
if err != nil {
|
if _, ok := err.(BucketExists); ok {
|
||||||
|
// Though bucket exists locally, we send the site-replication
|
||||||
|
// hook to ensure all sites have this bucket. If the hook
|
||||||
|
// succeeds, the client will still receive a bucket exists
|
||||||
|
// message.
|
||||||
|
globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts)
|
||||||
|
}
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -781,6 +822,12 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
// Load updated bucket metadata into memory.
|
// Load updated bucket metadata into memory.
|
||||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
||||||
|
|
||||||
|
// Call site replication hook
|
||||||
|
if err := globalSiteReplicationSys.MakeBucketHook(ctx, bucket, opts); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure to add Location information here only for bucket
|
// Make sure to add Location information here only for bucket
|
||||||
if cp := pathClean(r.URL.Path); cp != "" {
|
if cp := pathClean(r.URL.Path); cp != "" {
|
||||||
w.Header().Set(xhttp.Location, cp) // Clean any trailing slashes.
|
w.Header().Set(xhttp.Location, cp) // Clean any trailing slashes.
|
||||||
@@ -818,7 +865,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := crypto.IsRequested(r.Header); !objectAPI.IsEncryptionSupported() && ok {
|
if crypto.Requested(r.Header) && !objectAPI.IsEncryptionSupported() {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -885,7 +932,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
if fileName != "" && strings.Contains(formValues.Get("Key"), "${filename}") {
|
||||||
// S3 feature to replace ${filename} found in Key form field
|
// S3 feature to replace ${filename} found in Key form field
|
||||||
// by the filename attribute passed in multipart
|
// by the filename attribute passed in multipart
|
||||||
formValues.Set("Key", strings.Replace(formValues.Get("Key"), "${filename}", fileName, -1))
|
formValues.Set("Key", strings.ReplaceAll(formValues.Get("Key"), "${filename}", fileName))
|
||||||
}
|
}
|
||||||
object := trimLeadingSlash(formValues.Get("Key"))
|
object := trimLeadingSlash(formValues.Get("Key"))
|
||||||
|
|
||||||
@@ -911,6 +958,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
// explicit permissions for the user.
|
// explicit permissions for the user.
|
||||||
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
if !globalIAMSys.IsAllowed(iampolicy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
Action: iampolicy.PutObjectAction,
|
Action: iampolicy.PutObjectAction,
|
||||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
||||||
BucketName: bucket,
|
BucketName: bucket,
|
||||||
@@ -980,7 +1028,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
|
|
||||||
// Check if bucket encryption is enabled
|
// Check if bucket encryption is enabled
|
||||||
sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
|
sseConfig, _ := globalBucketSSEConfigSys.Get(bucket)
|
||||||
sseConfig.Apply(r.Header, globalAutoEncryption)
|
sseConfig.Apply(r.Header, sse.ApplyOptions{
|
||||||
|
AutoEncrypt: globalAutoEncryption,
|
||||||
|
Passthrough: globalIsGateway && globalGatewayName == S3BackendGateway,
|
||||||
|
})
|
||||||
|
|
||||||
// get gateway encryption options
|
// get gateway encryption options
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
@@ -990,7 +1041,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if objectAPI.IsEncryptionSupported() {
|
if objectAPI.IsEncryptionSupported() {
|
||||||
if _, ok := crypto.IsRequested(formValues); ok && !HasSuffix(object, SlashSeparator) { // handle SSE requests
|
if crypto.Requested(formValues) && !HasSuffix(object, SlashSeparator) { // handle SSE requests
|
||||||
if crypto.SSECopy.IsRequested(r.Header) {
|
if crypto.SSECopy.IsRequested(r.Header) {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParameters), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1065,9 +1116,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
Host: handlers.GetSourceIP(r),
|
Host: handlers.GetSourceIP(r),
|
||||||
})
|
})
|
||||||
|
|
||||||
if successRedirect != "" {
|
if redirectURL != nil { // success_action_redirect is valid and set.
|
||||||
// Replace raw query params..
|
redirectURL.Query().Add("bucket", objInfo.Bucket)
|
||||||
redirectURL.RawQuery = getRedirectPostRawQuery(objInfo)
|
redirectURL.Query().Add("key", objInfo.Name)
|
||||||
|
redirectURL.Query().Add("etag", "\""+objInfo.ETag+"\"")
|
||||||
writeRedirectSeeOther(w, redirectURL.String())
|
writeRedirectSeeOther(w, redirectURL.String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1179,7 +1231,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeResponse(w, http.StatusOK, nil, mimeXML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBucketHandler - Delete bucket
|
// DeleteBucketHandler - Delete bucket
|
||||||
@@ -1226,6 +1278,17 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rcfg, err := getReplicationConfig(ctx, bucket)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
if _, ok := err.(BucketReplicationConfigNotFound); !ok {
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case rcfg.HasActiveRules("", true):
|
||||||
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1240,7 +1303,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
deleteBucket := objectAPI.DeleteBucket
|
deleteBucket := objectAPI.DeleteBucket
|
||||||
|
|
||||||
// Attempt to delete bucket.
|
// Attempt to delete bucket.
|
||||||
if err := deleteBucket(ctx, bucket, forceDelete); err != nil {
|
if err := deleteBucket(ctx, bucket, DeleteBucketOptions{Force: forceDelete}); err != nil {
|
||||||
apiErr := toAPIError(ctx, err)
|
apiErr := toAPIError(ctx, err)
|
||||||
if _, ok := err.(BucketNotEmpty); ok {
|
if _, ok := err.(BucketNotEmpty); ok {
|
||||||
if globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket) {
|
if globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket) {
|
||||||
@@ -1249,7 +1312,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
if globalDNSConfig != nil {
|
if globalDNSConfig != nil {
|
||||||
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
|
if err2 := globalDNSConfig.Put(bucket); err2 != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, pl1ease fix it manually", err2))
|
logger.LogIf(ctx, fmt.Errorf("Unable to restore bucket DNS entry %w, please fix it manually", err2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||||
@@ -1257,6 +1320,12 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
|
|
||||||
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
globalNotificationSys.DeleteBucketMetadata(ctx, bucket)
|
||||||
|
globalReplicationPool.deleteResyncMetadata(ctx, bucket)
|
||||||
|
// Call site replication hook.
|
||||||
|
if err := globalSiteReplicationSys.DeleteBucketHook(ctx, bucket, forceDelete); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessNoContent(w)
|
writeSuccessNoContent(w)
|
||||||
@@ -1289,7 +1358,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !globalIsErasure {
|
if globalIsGateway {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1313,12 +1382,28 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||||
if _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, objectLockConfig, configData); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, objectLockConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeObjectLockConfig,
|
||||||
|
Bucket: bucket,
|
||||||
|
ObjectLockConfig: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1352,7 +1437,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1384,6 +1469,12 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if bucket exists.
|
||||||
|
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1403,7 +1494,23 @@ func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketTaggingConfig, configData); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
//
|
||||||
|
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||||
|
// errors.
|
||||||
|
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeTags,
|
||||||
|
Bucket: bucket,
|
||||||
|
Tags: &cfgStr,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1434,7 +1541,7 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
config, _, err := globalBucketMetadataSys.GetTaggingConfig(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1471,7 +1578,17 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := globalBucketMetadataSys.Update(bucket, bucketTaggingConfig, nil); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketTaggingConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypeTags,
|
||||||
|
Bucket: bucket,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1479,284 +1596,3 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
|||||||
// Write success response.
|
// Write success response.
|
||||||
writeSuccessResponseHeadersOnly(w)
|
writeSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
|
|
||||||
// ----------
|
|
||||||
// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
|
|
||||||
func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "PutBucketReplicationConfig")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !globalIsErasure {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
|
||||||
if err != nil {
|
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
|
||||||
apiErr.Description = err.Error()
|
|
||||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sameTarget, err := validateReplicationDestination(ctx, bucket, replicationConfig)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Validate the received bucket replication config
|
|
||||||
if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configData, err := xml.Marshal(replicationConfig)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketReplicationConfig, configData); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
|
|
||||||
// ----------
|
|
||||||
// Gets the replication configuration for a bucket.
|
|
||||||
func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "GetBucketReplicationConfig")
|
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user has permissions to perform this operation
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configData, err := xml.Marshal(config)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseXML(w, configData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
|
|
||||||
// ----------
|
|
||||||
func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "DeleteBucketReplicationConfig")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := globalBucketMetadataSys.Update(bucket, bucketReplicationConfig, nil); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBucketReplicationMetricsHandler - GET Bucket replication metrics.
|
|
||||||
// ----------
|
|
||||||
// Gets the replication metrics for a bucket.
|
|
||||||
func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "GetBucketReplicationMetrics")
|
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if user has permissions to perform this operation
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bucketStats := globalNotificationSys.GetClusterBucketStats(r.Context(), bucket)
|
|
||||||
bucketReplStats := BucketReplicationStats{}
|
|
||||||
// sum up metrics from each node in the cluster
|
|
||||||
for _, bucketStat := range bucketStats {
|
|
||||||
bucketReplStats.FailedCount += bucketStat.ReplicationStats.FailedCount
|
|
||||||
bucketReplStats.FailedSize += bucketStat.ReplicationStats.FailedSize
|
|
||||||
bucketReplStats.PendingCount += bucketStat.ReplicationStats.PendingCount
|
|
||||||
bucketReplStats.PendingSize += bucketStat.ReplicationStats.PendingSize
|
|
||||||
bucketReplStats.ReplicaSize += bucketStat.ReplicationStats.ReplicaSize
|
|
||||||
bucketReplStats.ReplicatedSize += bucketStat.ReplicationStats.ReplicatedSize
|
|
||||||
}
|
|
||||||
// add initial usage from the time of cluster up
|
|
||||||
usageStat := globalReplicationStats.GetInitialUsage(bucket)
|
|
||||||
bucketReplStats.FailedCount += usageStat.FailedCount
|
|
||||||
bucketReplStats.FailedSize += usageStat.FailedSize
|
|
||||||
bucketReplStats.PendingCount += usageStat.PendingCount
|
|
||||||
bucketReplStats.PendingSize += usageStat.PendingSize
|
|
||||||
bucketReplStats.ReplicaSize += usageStat.ReplicaSize
|
|
||||||
bucketReplStats.ReplicatedSize += usageStat.ReplicatedSize
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(&bucketReplStats); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetBucketReplicationStateHandler - starts a replication reset for all objects in a bucket which
|
|
||||||
// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
|
|
||||||
// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
|
|
||||||
// remote target is entirely lost,and previously replicated objects need to be re-synced.
|
|
||||||
func (api objectAPIHandlers) ResetBucketReplicationStateHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "ResetBucketReplicationState")
|
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
bucket := vars["bucket"]
|
|
||||||
durationStr := r.Form.Get("older-than")
|
|
||||||
var (
|
|
||||||
days time.Duration
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if durationStr != "" {
|
|
||||||
days, err = time.ParseDuration(durationStr)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
|
|
||||||
Bucket: bucket,
|
|
||||||
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
|
||||||
}), r.URL)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
objectAPI := api.ObjectAPI()
|
|
||||||
if objectAPI == nil {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket exists.
|
|
||||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !config.HasActiveRules("", true) {
|
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoMatchingRuleError), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, config.RoleArn)
|
|
||||||
target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
|
|
||||||
target.ResetID = mustGetUUID()
|
|
||||||
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case BucketRemoteConnectionErr:
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
|
|
||||||
default:
|
|
||||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
targets, err := globalBucketTargetSys.ListBucketTargets(ctx, bucket)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgtBytes, err := json.Marshal(&targets)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketTargetsFile, tgtBytes); err != nil {
|
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(target.ResetID)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Write success response.
|
|
||||||
writeSuccessResponseJSON(w, data)
|
|
||||||
}
|
|
||||||
|
|||||||
+77
-22
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -35,7 +36,8 @@ func TestRemoveBucketHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
_, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewReader([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
_, err := obj.PutObject(GlobalContext, bucketName, "test-object", mustGetPutObjReader(t, bytes.NewReader([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
||||||
// if object upload fails stop the test.
|
// if object upload fails stop the test.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,8 +82,8 @@ func TestGetBucketLocationHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// test cases with sample input and expected output.
|
// test cases with sample input and expected output.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
@@ -162,7 +164,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2(http.MethodGet, getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
reqV2, err := newTestSignedRequestV2(http.MethodGet, getBucketLocationURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -209,7 +210,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
|
|
||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
nilReq, err := newTestRequest(http.MethodGet, getBucketLocationURL("", nilBucket), 0, nil)
|
nilReq, err := newTestRequest(http.MethodGet, getBucketLocationURL("", nilBucket), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -224,8 +224,8 @@ func TestHeadBucketHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// test cases with sample input and expected output.
|
// test cases with sample input and expected output.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
@@ -281,7 +281,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
recV2 := httptest.NewRecorder()
|
recV2 := httptest.NewRecorder()
|
||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV2, err := newTestSignedRequestV2(http.MethodHead, getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
reqV2, err := newTestSignedRequestV2(http.MethodHead, getHEADBucketURL("", testCase.bucketName), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -296,7 +295,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
|
|
||||||
// Test for Anonymous/unsigned http request.
|
// Test for Anonymous/unsigned http request.
|
||||||
anonReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", bucketName), 0, nil)
|
anonReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", bucketName), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
||||||
instanceType, bucketName, err)
|
instanceType, bucketName, err)
|
||||||
@@ -314,7 +312,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
|
|
||||||
nilBucket := "dummy-bucket"
|
nilBucket := "dummy-bucket"
|
||||||
nilReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", nilBucket), 0, nil)
|
nilReq, err := newTestRequest(http.MethodHead, getHEADBucketURL("", nilBucket), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -330,8 +327,8 @@ func TestListMultipartUploadsHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
// testListMultipartUploadsHandler - Tests validate listing of multipart uploads.
|
||||||
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
// Collection of non-exhaustive ListMultipartUploads test cases, valid errors
|
||||||
// and success responses.
|
// and success responses.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -551,7 +548,6 @@ func testListMultipartUploadsHandler(obj ObjectLayer, instanceType, bucketName s
|
|||||||
testCases[6].uploadIDMarker, testCases[6].delimiter, testCases[6].maxUploads)
|
testCases[6].uploadIDMarker, testCases[6].delimiter, testCases[6].maxUploads)
|
||||||
|
|
||||||
nilReq, err := newTestRequest(http.MethodGet, url, 0, nil)
|
nilReq, err := newTestRequest(http.MethodGet, url, 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -567,8 +563,8 @@ func TestListBucketsHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testListBucketsHandler - Tests validate listing of buckets.
|
// testListBucketsHandler - Tests validate listing of buckets.
|
||||||
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
accessKey string
|
accessKey string
|
||||||
@@ -614,7 +610,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
|
|
||||||
// verify response for V2 signed HTTP request.
|
// verify response for V2 signed HTTP request.
|
||||||
reqV2, err := newTestSignedRequestV2(http.MethodGet, getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
reqV2, err := newTestSignedRequestV2(http.MethodGet, getListBucketURL(""), 0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
t.Fatalf("Test %d: %s: Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", i+1, instanceType, err)
|
||||||
}
|
}
|
||||||
@@ -629,7 +624,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
// Test for Anonymous/unsigned http request.
|
// Test for Anonymous/unsigned http request.
|
||||||
// ListBucketsHandler doesn't support bucket policies, setting the policies shouldn't make a difference.
|
// ListBucketsHandler doesn't support bucket policies, setting the policies shouldn't make a difference.
|
||||||
anonReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
|
anonReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MinIO %s: Failed to create an anonymous request.", instanceType)
|
t.Fatalf("MinIO %s: Failed to create an anonymous request.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -645,7 +639,6 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
|
// The only aim is to generate an HTTP request in a way that the relevant/registered end point is evoked/called.
|
||||||
|
|
||||||
nilReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
|
nilReq, err := newTestRequest(http.MethodGet, getListBucketURL(""), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -656,12 +649,12 @@ func testListBucketsHandler(obj ObjectLayer, instanceType, bucketName string, ap
|
|||||||
|
|
||||||
// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling DeleteMultipleObjects HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
func TestAPIDeleteMultipleObjectsHandler(t *testing.T) {
|
||||||
ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects"})
|
ExecObjectLayerAPITest(t, testAPIDeleteMultipleObjectsHandler, []string{"DeleteMultipleObjects", "PutBucketPolicy"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
contentBytes := []byte("hello")
|
contentBytes := []byte("hello")
|
||||||
@@ -680,10 +673,35 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
objectNames = append(objectNames, objectName)
|
objectNames = append(objectNames, objectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{"private/object", "public/object"} {
|
||||||
|
// Uploading the object with retention enabled
|
||||||
|
_, err = obj.PutObject(GlobalContext, bucketName, name, mustGetPutObjReader(t, bytes.NewReader(contentBytes), int64(len(contentBytes)), "", sha256sum), ObjectOptions{})
|
||||||
|
// if object upload fails stop the test.
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Put Object %s: Error uploading object: <ERROR> %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following block will create a bucket policy with delete object to 'public/*'. This is
|
||||||
|
// to test a mixed response of a successful & failure while deleting objects in a single request
|
||||||
|
policyBytes := []byte(fmt.Sprintf(`{"Id": "Policy1637752602639", "Version": "2012-10-17", "Statement": [{"Sid": "Stmt1637752600730", "Action": "s3:DeleteObject", "Effect": "Allow", "Resource": "arn:aws:s3:::%s/public/*", "Principal": "*"}]}`, bucketName))
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", bucketName), int64(len(policyBytes)), bytes.NewReader(policyBytes),
|
||||||
|
credentials.AccessKey, credentials.SecretKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create HTTP request for PutBucketPolicyHandler: <ERROR> %v", err)
|
||||||
|
}
|
||||||
|
apiRouter.ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusNoContent {
|
||||||
|
t.Errorf("Expected the response status to be `%d`, but instead found `%d`", 200, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
getObjectToDeleteList := func(objectNames []string) (objectList []ObjectToDelete) {
|
getObjectToDeleteList := func(objectNames []string) (objectList []ObjectToDelete) {
|
||||||
for _, objectName := range objectNames {
|
for _, objectName := range objectNames {
|
||||||
objectList = append(objectList, ObjectToDelete{
|
objectList = append(objectList, ObjectToDelete{
|
||||||
ObjectName: objectName,
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: objectName,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,9 +720,21 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
return deleteErrorList
|
return deleteErrorList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
objects := []ObjectToDelete{}
|
||||||
|
objects = append(objects, ObjectToDelete{
|
||||||
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: "private/object",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
objects = append(objects, ObjectToDelete{
|
||||||
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: "public/object",
|
||||||
|
},
|
||||||
|
})
|
||||||
requestList := []DeleteObjectsRequest{
|
requestList := []DeleteObjectsRequest{
|
||||||
{Quiet: false, Objects: getObjectToDeleteList(objectNames[:5])},
|
{Quiet: false, Objects: getObjectToDeleteList(objectNames[:5])},
|
||||||
{Quiet: true, Objects: getObjectToDeleteList(objectNames[5:])},
|
{Quiet: true, Objects: getObjectToDeleteList(objectNames[5:])},
|
||||||
|
{Quiet: false, Objects: objects},
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate multi objects delete response.
|
// generate multi objects delete response.
|
||||||
@@ -741,6 +771,20 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
anonResponse := generateMultiDeleteResponse(requestList[0].Quiet, nil, getDeleteErrorList(requestList[0].Objects))
|
anonResponse := generateMultiDeleteResponse(requestList[0].Quiet, nil, getDeleteErrorList(requestList[0].Objects))
|
||||||
encodedAnonResponse := encodeResponse(anonResponse)
|
encodedAnonResponse := encodeResponse(anonResponse)
|
||||||
|
|
||||||
|
anonRequestWithPartialPublicAccess := encodeResponse(requestList[2])
|
||||||
|
anonResponseWithPartialPublicAccess := generateMultiDeleteResponse(requestList[2].Quiet,
|
||||||
|
[]DeletedObject{
|
||||||
|
{ObjectName: "public/object"},
|
||||||
|
},
|
||||||
|
[]DeleteError{
|
||||||
|
{
|
||||||
|
Code: errorCodes[ErrAccessDenied].Code,
|
||||||
|
Message: errorCodes[ErrAccessDenied].Description,
|
||||||
|
Key: "private/object",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
encodedAnonResponseWithPartialPublicAccess := encodeResponse(anonResponseWithPartialPublicAccess)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
bucket string
|
bucket string
|
||||||
objects []byte
|
objects []byte
|
||||||
@@ -800,6 +844,17 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
|||||||
expectedContent: encodedAnonResponse,
|
expectedContent: encodedAnonResponse,
|
||||||
expectedRespStatus: http.StatusOK,
|
expectedRespStatus: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
// Test case - 6.
|
||||||
|
// Anonymous user has access to some public folder, issue removing with
|
||||||
|
// another private object as well
|
||||||
|
{
|
||||||
|
bucket: bucketName,
|
||||||
|
objects: anonRequestWithPartialPublicAccess,
|
||||||
|
accessKey: "",
|
||||||
|
secretKey: "",
|
||||||
|
expectedContent: encodedAnonResponseWithPartialPublicAccess,
|
||||||
|
expectedRespStatus: http.StatusOK,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketLifecycleConfig, configData); err != nil {
|
if _, err = globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, configData); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ func (api objectAPIHandlers) DeleteBucketLifecycleHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := globalBucketMetadataSys.Update(bucket, bucketLifecycleConfig, nil); err != nil {
|
if _, err := globalBucketMetadataSys.Update(ctx, bucket, bucketLifecycleConfig, nil); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
|||||||
|
|
||||||
// Test for authentication
|
// Test for authentication
|
||||||
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// test cases with sample input and expected output.
|
// test cases with sample input and expected output.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
method string
|
method string
|
||||||
@@ -150,8 +151,8 @@ func TestBucketLifecycle(t *testing.T) {
|
|||||||
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
||||||
// Tests are related and the order is important.
|
// Tests are related and the order is important.
|
||||||
func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
creds auth.Credentials, t *testing.T) {
|
creds auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// test cases with sample input and expected output.
|
// test cases with sample input and expected output.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
method string
|
method string
|
||||||
@@ -266,8 +267,8 @@ func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRo
|
|||||||
lifecycleResponse []byte
|
lifecycleResponse []byte
|
||||||
errorResponse APIErrorResponse
|
errorResponse APIErrorResponse
|
||||||
shouldPass bool
|
shouldPass bool
|
||||||
}) {
|
},
|
||||||
|
) {
|
||||||
for i, testCase := range testCases {
|
for i, testCase := range testCases {
|
||||||
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|||||||
+147
-64
@@ -75,48 +75,83 @@ func NewLifecycleSys() *LifecycleSys {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type expiryTask struct {
|
type expiryTask struct {
|
||||||
objInfo ObjectInfo
|
objInfo ObjectInfo
|
||||||
versionExpiry bool
|
versionExpiry bool
|
||||||
|
restoredObject bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type expiryState struct {
|
type expiryState struct {
|
||||||
once sync.Once
|
once sync.Once
|
||||||
expiryCh chan expiryTask
|
byDaysCh chan expiryTask
|
||||||
|
byNewerNoncurrentCh chan newerNoncurrentTask
|
||||||
}
|
}
|
||||||
|
|
||||||
// PendingTasks returns the number of pending ILM expiry tasks.
|
// PendingTasks returns the number of pending ILM expiry tasks.
|
||||||
func (es *expiryState) PendingTasks() int {
|
func (es *expiryState) PendingTasks() int {
|
||||||
return len(es.expiryCh)
|
return len(es.byDaysCh) + len(es.byNewerNoncurrentCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *expiryState) queueExpiryTask(oi ObjectInfo, rmVersion bool) {
|
// close closes work channels exactly once.
|
||||||
|
func (es *expiryState) close() {
|
||||||
|
es.once.Do(func() {
|
||||||
|
close(es.byDaysCh)
|
||||||
|
close(es.byNewerNoncurrentCh)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// enqueueByDays enqueues object versions expired by days for expiry.
|
||||||
|
func (es *expiryState) enqueueByDays(oi ObjectInfo, restoredObject bool, rmVersion bool) {
|
||||||
select {
|
select {
|
||||||
case <-GlobalContext.Done():
|
case <-GlobalContext.Done():
|
||||||
es.once.Do(func() {
|
es.close()
|
||||||
close(es.expiryCh)
|
case es.byDaysCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion, restoredObject: restoredObject}:
|
||||||
})
|
|
||||||
case es.expiryCh <- expiryTask{objInfo: oi, versionExpiry: rmVersion}:
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// enqueueByNewerNoncurrent enqueues object versions expired by
|
||||||
globalExpiryState *expiryState
|
// NewerNoncurrentVersions limit for expiry.
|
||||||
)
|
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete) {
|
||||||
|
select {
|
||||||
|
case <-GlobalContext.Done():
|
||||||
|
es.close()
|
||||||
|
case es.byNewerNoncurrentCh <- newerNoncurrentTask{bucket: bucket, versions: versions}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalExpiryState *expiryState
|
||||||
|
|
||||||
func newExpiryState() *expiryState {
|
func newExpiryState() *expiryState {
|
||||||
return &expiryState{
|
return &expiryState{
|
||||||
expiryCh: make(chan expiryTask, 10000),
|
byDaysCh: make(chan expiryTask, 10000),
|
||||||
|
byNewerNoncurrentCh: make(chan newerNoncurrentTask, 10000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
||||||
globalExpiryState = newExpiryState()
|
globalExpiryState = newExpiryState()
|
||||||
go func() {
|
go func() {
|
||||||
for t := range globalExpiryState.expiryCh {
|
for t := range globalExpiryState.byDaysCh {
|
||||||
applyExpiryRule(ctx, objectAPI, t.objInfo, false, t.versionExpiry)
|
if t.objInfo.TransitionedObject.Status != "" {
|
||||||
|
applyExpiryOnTransitionedObject(ctx, objectAPI, t.objInfo, t.restoredObject)
|
||||||
|
} else {
|
||||||
|
applyExpiryOnNonTransitionedObjects(ctx, objectAPI, t.objInfo, t.versionExpiry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
go func() {
|
||||||
|
for t := range globalExpiryState.byNewerNoncurrentCh {
|
||||||
|
deleteObjectVersions(ctx, objectAPI, t.bucket, t.versions)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newerNoncurrentTask encapsulates arguments required by worker to expire objects
|
||||||
|
// by NewerNoncurrentVersions
|
||||||
|
type newerNoncurrentTask struct {
|
||||||
|
bucket string
|
||||||
|
versions []ObjectToDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
type transitionState struct {
|
type transitionState struct {
|
||||||
@@ -130,6 +165,9 @@ type transitionState struct {
|
|||||||
killCh chan struct{}
|
killCh chan struct{}
|
||||||
|
|
||||||
activeTasks int32
|
activeTasks int32
|
||||||
|
|
||||||
|
lastDayMu sync.RWMutex
|
||||||
|
lastDayStats map[string]*lastDayTierStats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
|
func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
|
||||||
@@ -143,9 +181,7 @@ func (t *transitionState) queueTransitionTask(oi ObjectInfo) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var globalTransitionState *transitionState
|
||||||
globalTransitionState *transitionState
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionState {
|
func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionState {
|
||||||
return &transitionState{
|
return &transitionState{
|
||||||
@@ -153,6 +189,7 @@ func newTransitionState(ctx context.Context, objAPI ObjectLayer) *transitionStat
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
objAPI: objAPI,
|
objAPI: objAPI,
|
||||||
killCh: make(chan struct{}),
|
killCh: make(chan struct{}),
|
||||||
|
lastDayStats: make(map[string]*lastDayTierStats),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,14 +217,47 @@ func (t *transitionState) worker(ctx context.Context, objectAPI ObjectLayer) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
atomic.AddInt32(&t.activeTasks, 1)
|
atomic.AddInt32(&t.activeTasks, 1)
|
||||||
if err := transitionObject(ctx, objectAPI, oi); err != nil {
|
var tier string
|
||||||
|
var err error
|
||||||
|
if tier, err = transitionObject(ctx, objectAPI, oi); err != nil {
|
||||||
logger.LogIf(ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w", oi.Bucket, oi.Name, oi.VersionID, err))
|
logger.LogIf(ctx, fmt.Errorf("Transition failed for %s/%s version:%s with %w", oi.Bucket, oi.Name, oi.VersionID, err))
|
||||||
|
} else {
|
||||||
|
ts := tierStats{
|
||||||
|
TotalSize: uint64(oi.Size),
|
||||||
|
NumVersions: 1,
|
||||||
|
}
|
||||||
|
if oi.IsLatest {
|
||||||
|
ts.NumObjects = 1
|
||||||
|
}
|
||||||
|
t.addLastDayStats(tier, ts)
|
||||||
}
|
}
|
||||||
atomic.AddInt32(&t.activeTasks, -1)
|
atomic.AddInt32(&t.activeTasks, -1)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transitionState) addLastDayStats(tier string, ts tierStats) {
|
||||||
|
t.lastDayMu.Lock()
|
||||||
|
defer t.lastDayMu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := t.lastDayStats[tier]; !ok {
|
||||||
|
t.lastDayStats[tier] = &lastDayTierStats{}
|
||||||
|
}
|
||||||
|
t.lastDayStats[tier].addStats(ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transitionState) getDailyAllTierStats() DailyAllTierStats {
|
||||||
|
t.lastDayMu.RLock()
|
||||||
|
defer t.lastDayMu.RUnlock()
|
||||||
|
|
||||||
|
res := make(DailyAllTierStats, len(t.lastDayStats))
|
||||||
|
for tier, st := range t.lastDayStats {
|
||||||
|
res[tier] = st.clone()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateWorkers at the end of this function leaves n goroutines waiting for
|
// UpdateWorkers at the end of this function leaves n goroutines waiting for
|
||||||
// transition tasks
|
// transition tasks
|
||||||
func (t *transitionState) UpdateWorkers(n int) {
|
func (t *transitionState) UpdateWorkers(n int) {
|
||||||
@@ -215,16 +285,31 @@ var errInvalidStorageClass = errors.New("invalid storage class")
|
|||||||
|
|
||||||
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
func validateTransitionTier(lc *lifecycle.Lifecycle) error {
|
||||||
for _, rule := range lc.Rules {
|
for _, rule := range lc.Rules {
|
||||||
if rule.Transition.StorageClass == "" {
|
if rule.Transition.StorageClass != "" {
|
||||||
continue
|
if valid := globalTierConfigMgr.IsTierValid(rule.Transition.StorageClass); !valid {
|
||||||
|
return errInvalidStorageClass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if valid := globalTierConfigMgr.IsTierValid(rule.Transition.StorageClass); !valid {
|
if rule.NoncurrentVersionTransition.StorageClass != "" {
|
||||||
return errInvalidStorageClass
|
if valid := globalTierConfigMgr.IsTierValid(rule.NoncurrentVersionTransition.StorageClass); !valid {
|
||||||
|
return errInvalidStorageClass
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// enqueueTransitionImmediate enqueues obj for transition if eligible.
|
||||||
|
// This is to be called after a successful upload of an object (version).
|
||||||
|
func enqueueTransitionImmediate(obj ObjectInfo) {
|
||||||
|
if lc, err := globalLifecycleSys.Get(obj.Bucket); err == nil {
|
||||||
|
switch lc.ComputeAction(obj.ToLifecycleOpts()) {
|
||||||
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
|
globalTransitionState.queueTransitionTask(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// expireAction represents different actions to be performed on expiry of a
|
// expireAction represents different actions to be performed on expiry of a
|
||||||
// restored/transitioned object
|
// restored/transitioned object
|
||||||
type expireAction int
|
type expireAction int
|
||||||
@@ -245,8 +330,9 @@ const (
|
|||||||
// 2. when a transitioned object expires (based on an ILM rule).
|
// 2. when a transitioned object expires (based on an ILM rule).
|
||||||
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, action expireAction) error {
|
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, action expireAction) error {
|
||||||
var opts ObjectOptions
|
var opts ObjectOptions
|
||||||
opts.Versioned = globalBucketVersioningSys.Enabled(oi.Bucket)
|
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name)
|
||||||
opts.VersionID = lcOpts.VersionID
|
opts.VersionID = lcOpts.VersionID
|
||||||
|
opts.Expiration = ExpirationOptions{Expire: true}
|
||||||
switch action {
|
switch action {
|
||||||
case expireObj:
|
case expireObj:
|
||||||
// When an object is past expiry or when a transitioned object is being
|
// When an object is past expiry or when a transitioned object is being
|
||||||
@@ -316,22 +402,24 @@ func genTransitionObjName(bucket string) (string, error) {
|
|||||||
// storage specified by the transition ARN, the metadata is left behind on source cluster and original content
|
// storage specified by the transition ARN, the metadata is left behind on source cluster and original content
|
||||||
// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
|
// is moved to the transition tier. Note that in the case of encrypted objects, entire encrypted stream is moved
|
||||||
// to the transition tier without decrypting or re-encrypting.
|
// to the transition tier without decrypting or re-encrypting.
|
||||||
func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo) error {
|
func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo) (string, error) {
|
||||||
lc, err := globalLifecycleSys.Get(oi.Bucket)
|
lc, err := globalLifecycleSys.Get(oi.Bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
tier := lc.TransitionTier(oi.ToLifecycleOpts())
|
||||||
opts := ObjectOptions{
|
opts := ObjectOptions{
|
||||||
Transition: TransitionOptions{
|
Transition: TransitionOptions{
|
||||||
Status: lifecycle.TransitionPending,
|
Status: lifecycle.TransitionPending,
|
||||||
Tier: lc.TransitionTier(oi.ToLifecycleOpts()),
|
Tier: tier,
|
||||||
ETag: oi.ETag,
|
ETag: oi.ETag,
|
||||||
},
|
},
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
Versioned: globalBucketVersioningSys.Enabled(oi.Bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
||||||
MTime: oi.ModTime,
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
||||||
|
MTime: oi.ModTime,
|
||||||
}
|
}
|
||||||
return objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
return tier, objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTransitionedObjectReader returns a reader from the transitioned tier.
|
// getTransitionedObjectReader returns a reader from the transitioned tier.
|
||||||
@@ -373,9 +461,9 @@ const (
|
|||||||
|
|
||||||
// Encryption specifies encryption setting on restored bucket
|
// Encryption specifies encryption setting on restored bucket
|
||||||
type Encryption struct {
|
type Encryption struct {
|
||||||
EncryptionType sse.SSEAlgorithm `xml:"EncryptionType"`
|
EncryptionType sse.Algorithm `xml:"EncryptionType"`
|
||||||
KMSContext string `xml:"KMSContext,omitempty"`
|
KMSContext string `xml:"KMSContext,omitempty"`
|
||||||
KMSKeyID string `xml:"KMSKeyId,omitempty"`
|
KMSKeyID string `xml:"KMSKeyId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetadataEntry denotes name and value.
|
// MetadataEntry denotes name and value.
|
||||||
@@ -414,9 +502,7 @@ func (sp *SelectParameters) IsEmpty() bool {
|
|||||||
return sp == nil
|
return sp == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var selectParamsXMLName = "SelectParameters"
|
||||||
selectParamsXMLName = "SelectParameters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnmarshalXML - decodes XML data.
|
// UnmarshalXML - decodes XML data.
|
||||||
func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
func (sp *SelectParameters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
@@ -489,8 +575,8 @@ func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer)
|
|||||||
|
|
||||||
// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
|
// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
|
||||||
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
||||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||||
versionSuspended := globalBucketVersioningSys.Suspended(bucket)
|
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||||
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
|
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
|
||||||
if vid != "" && vid != nullVersionID {
|
if vid != "" && vid != nullVersionID {
|
||||||
_, err := uuid.Parse(vid)
|
_, err := uuid.Parse(vid)
|
||||||
@@ -528,7 +614,7 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
|||||||
|
|
||||||
if rreq.Type == SelectRestoreRequest {
|
if rreq.Type == SelectRestoreRequest {
|
||||||
for _, v := range rreq.OutputLocation.S3.UserMetadata {
|
for _, v := range rreq.OutputLocation.S3.UserMetadata {
|
||||||
if !strings.HasPrefix("x-amz-meta", strings.ToLower(v.Name)) {
|
if !strings.HasPrefix(strings.ToLower(v.Name), "x-amz-meta") {
|
||||||
meta["x-amz-meta-"+v.Name] = v.Value
|
meta["x-amz-meta-"+v.Name] = v.Value
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -541,8 +627,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
|||||||
meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
||||||
}
|
}
|
||||||
return ObjectOptions{
|
return ObjectOptions{
|
||||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||||
UserDefined: meta,
|
UserDefined: meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,8 +640,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ObjectOptions{
|
return ObjectOptions{
|
||||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||||
UserDefined: meta,
|
UserDefined: meta,
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
MTime: objInfo.ModTime,
|
MTime: objInfo.ModTime,
|
||||||
@@ -608,9 +694,9 @@ func completedRestoreObj(expiry time.Time) restoreObjStatus {
|
|||||||
// String returns x-amz-restore compatible representation of r.
|
// String returns x-amz-restore compatible representation of r.
|
||||||
func (r restoreObjStatus) String() string {
|
func (r restoreObjStatus) String() string {
|
||||||
if r.Ongoing() {
|
if r.Ongoing() {
|
||||||
return "ongoing-request=true"
|
return `ongoing-request="true"`
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("ongoing-request=false, expiry-date=%s", r.expiry.Format(http.TimeFormat))
|
return fmt.Sprintf(`ongoing-request="false", expiry-date="%s"`, r.expiry.Format(http.TimeFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expiry returns expiry of restored object and true if restore-object has completed.
|
// Expiry returns expiry of restored object and true if restore-object has completed.
|
||||||
@@ -654,12 +740,11 @@ func parseRestoreObjStatus(restoreHdr string) (restoreObjStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch progressTokens[1] {
|
switch progressTokens[1] {
|
||||||
case "true":
|
case "true", `"true"`: // true without double quotes is deprecated in Feb 2022
|
||||||
if len(tokens) == 1 {
|
if len(tokens) == 1 {
|
||||||
return ongoingRestoreObj(), nil
|
return ongoingRestoreObj(), nil
|
||||||
}
|
}
|
||||||
|
case "false", `"false"`: // false without double quotes is deprecated in Feb 2022
|
||||||
case "false":
|
|
||||||
if len(tokens) != 2 {
|
if len(tokens) != 2 {
|
||||||
return restoreObjStatus{}, errRestoreHDRMalformed
|
return restoreObjStatus{}, errRestoreHDRMalformed
|
||||||
}
|
}
|
||||||
@@ -670,8 +755,7 @@ func parseRestoreObjStatus(restoreHdr string) (restoreObjStatus, error) {
|
|||||||
if strings.TrimSpace(expiryTokens[0]) != "expiry-date" {
|
if strings.TrimSpace(expiryTokens[0]) != "expiry-date" {
|
||||||
return restoreObjStatus{}, errRestoreHDRMalformed
|
return restoreObjStatus{}, errRestoreHDRMalformed
|
||||||
}
|
}
|
||||||
|
expiry, err := time.Parse(http.TimeFormat, strings.Trim(expiryTokens[1], `"`))
|
||||||
expiry, err := time.Parse(http.TimeFormat, expiryTokens[1])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return restoreObjStatus{}, errRestoreHDRMalformed
|
return restoreObjStatus{}, errRestoreHDRMalformed
|
||||||
}
|
}
|
||||||
@@ -695,17 +779,16 @@ func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
|
|||||||
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
|
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
|
||||||
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
||||||
return lifecycle.ObjectOpts{
|
return lifecycle.ObjectOpts{
|
||||||
Name: oi.Name,
|
Name: oi.Name,
|
||||||
UserTags: oi.UserTags,
|
UserTags: oi.UserTags,
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
ModTime: oi.ModTime,
|
ModTime: oi.ModTime,
|
||||||
IsLatest: oi.IsLatest,
|
IsLatest: oi.IsLatest,
|
||||||
NumVersions: oi.NumVersions,
|
NumVersions: oi.NumVersions,
|
||||||
DeleteMarker: oi.DeleteMarker,
|
DeleteMarker: oi.DeleteMarker,
|
||||||
SuccessorModTime: oi.SuccessorModTime,
|
SuccessorModTime: oi.SuccessorModTime,
|
||||||
RestoreOngoing: oi.RestoreOngoing,
|
RestoreOngoing: oi.RestoreOngoing,
|
||||||
RestoreExpires: oi.RestoreExpires,
|
RestoreExpires: oi.RestoreExpires,
|
||||||
TransitionStatus: oi.TransitionedObject.Status,
|
TransitionStatus: oi.TransitionedObject.Status,
|
||||||
RemoteTiersImmediately: globalDebugRemoteTiersImmediately,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestParseRestoreObjStatus(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
// valid: represents a restored object, 'pending' expiry.
|
// valid: represents a restored object, 'pending' expiry.
|
||||||
restoreHdr: "ongoing-request=false, expiry-date=Fri, 21 Dec 2012 00:00:00 GMT",
|
restoreHdr: `ongoing-request="false", expiry-date="Fri, 21 Dec 2012 00:00:00 GMT"`,
|
||||||
expectedStatus: restoreObjStatus{
|
expectedStatus: restoreObjStatus{
|
||||||
ongoing: false,
|
ongoing: false,
|
||||||
expiry: time.Date(2012, 12, 21, 0, 0, 0, 0, time.UTC),
|
expiry: time.Date(2012, 12, 21, 0, 0, 0, 0, time.UTC),
|
||||||
@@ -45,7 +45,7 @@ func TestParseRestoreObjStatus(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// valid: represents an ongoing restore object request.
|
// valid: represents an ongoing restore object request.
|
||||||
restoreHdr: "ongoing-request=true",
|
restoreHdr: `ongoing-request="true"`,
|
||||||
expectedStatus: restoreObjStatus{
|
expectedStatus: restoreObjStatus{
|
||||||
ongoing: true,
|
ongoing: true,
|
||||||
},
|
},
|
||||||
@@ -53,13 +53,13 @@ func TestParseRestoreObjStatus(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// invalid; ongoing restore object request can't have expiry set on it.
|
// invalid; ongoing restore object request can't have expiry set on it.
|
||||||
restoreHdr: "ongoing-request=true, expiry-date=Fri, 21 Dec 2012 00:00:00 GMT",
|
restoreHdr: `ongoing-request="true", expiry-date="Fri, 21 Dec 2012 00:00:00 GMT"`,
|
||||||
expectedStatus: restoreObjStatus{},
|
expectedStatus: restoreObjStatus{},
|
||||||
expectedErr: errRestoreHDRMalformed,
|
expectedErr: errRestoreHDRMalformed,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// invalid; completed restore object request must have expiry set on it.
|
// invalid; completed restore object request must have expiry set on it.
|
||||||
restoreHdr: "ongoing-request=false",
|
restoreHdr: `ongoing-request="false"`,
|
||||||
expectedStatus: restoreObjStatus{},
|
expectedStatus: restoreObjStatus{},
|
||||||
expectedErr: errRestoreHDRMalformed,
|
expectedErr: errRestoreHDRMalformed,
|
||||||
},
|
},
|
||||||
@@ -203,7 +203,7 @@ func TestObjectIsRemote(t *testing.T) {
|
|||||||
if got := fi.IsRemote(); got != tc.remote {
|
if got := fi.IsRemote(); got != tc.remote {
|
||||||
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
|
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
|
||||||
}
|
}
|
||||||
oi := fi.ToObjectInfo("bucket", "object")
|
oi := fi.ToObjectInfo("bucket", "object", false)
|
||||||
if got := oi.IsRemote(); got != tc.remote {
|
if got := oi.IsRemote(); got != tc.remote {
|
||||||
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
|
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,28 +26,9 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/sync/errgroup"
|
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
func concurrentDecryptETag(ctx context.Context, objects []ObjectInfo) {
|
|
||||||
g := errgroup.WithNErrs(len(objects)).WithConcurrency(500)
|
|
||||||
_, cancel := g.WithCancelOnError(ctx)
|
|
||||||
defer cancel()
|
|
||||||
for index := range objects {
|
|
||||||
index := index
|
|
||||||
g.Go(func() error {
|
|
||||||
size, err := objects[index].GetActualSize()
|
|
||||||
if err == nil {
|
|
||||||
objects[index].Size = size
|
|
||||||
}
|
|
||||||
objects[index].ETag = objects[index].GetActualETag(nil)
|
|
||||||
return nil
|
|
||||||
}, index)
|
|
||||||
}
|
|
||||||
g.WaitErr()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
||||||
// if one of the args do not meet the required conditions.
|
// if one of the args do not meet the required conditions.
|
||||||
// Special conditions required by MinIO server are as below
|
// Special conditions required by MinIO server are as below
|
||||||
@@ -61,8 +42,8 @@ func validateListObjectsArgs(marker, delimiter, encodingType string, maxKeys int
|
|||||||
}
|
}
|
||||||
|
|
||||||
if encodingType != "" {
|
if encodingType != "" {
|
||||||
// Only url encoding type is supported
|
// AWS S3 spec only supports 'url' encoding type
|
||||||
if strings.ToLower(encodingType) != "url" {
|
if !strings.EqualFold(encodingType, "url") {
|
||||||
return ErrInvalidEncodingMethod
|
return ErrInvalidEncodingMethod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +99,10 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectVersionsInfo.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectVersionsInfo.Objects); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
response := generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType, maxkeys, listObjectVersionsInfo)
|
||||||
|
|
||||||
@@ -128,7 +112,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
|||||||
|
|
||||||
// ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata.
|
// ListObjectsV2MHandler - GET Bucket (List Objects) Version 2 with metadata.
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// This implementation of the GET operation returns some or all (up to 10000)
|
// This implementation of the GET operation returns some or all (up to 1000)
|
||||||
// of the objects in a bucket. You can use the request parameters as selection
|
// of the objects in a bucket. You can use the request parameters as selection
|
||||||
// criteria to return a subset of the objects in a bucket.
|
// criteria to return a subset of the objects in a bucket.
|
||||||
//
|
//
|
||||||
@@ -180,7 +164,10 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// The next continuation token has id@node_index format to optimize paginated listing
|
// The next continuation token has id@node_index format to optimize paginated listing
|
||||||
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
nextContinuationToken := listObjectsV2Info.NextContinuationToken
|
||||||
@@ -195,7 +182,7 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
|||||||
|
|
||||||
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// This implementation of the GET operation returns some or all (up to 10000)
|
// This implementation of the GET operation returns some or all (up to 1000)
|
||||||
// of the objects in a bucket. You can use the request parameters as selection
|
// of the objects in a bucket. You can use the request parameters as selection
|
||||||
// criteria to return a subset of the objects in a bucket.
|
// criteria to return a subset of the objects in a bucket.
|
||||||
//
|
//
|
||||||
@@ -255,7 +242,10 @@ func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsV2Info.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsV2Info.Objects); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter,
|
||||||
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
delimiter, encodingType, fetchOwner, listObjectsV2Info.IsTruncated,
|
||||||
@@ -305,7 +295,7 @@ func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// This implementation of the GET operation returns some or all (up to 10000)
|
// This implementation of the GET operation returns some or all (up to 1000)
|
||||||
// of the objects in a bucket. You can use the request parameters as selection
|
// of the objects in a bucket. You can use the request parameters as selection
|
||||||
// criteria to return a subset of the objects in a bucket.
|
// criteria to return a subset of the objects in a bucket.
|
||||||
//
|
//
|
||||||
@@ -352,7 +342,10 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
concurrentDecryptETag(ctx, listObjectsInfo.Objects)
|
if err = DecryptETags(ctx, GlobalKMS, listObjectsInfo.Objects); err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType, maxKeys, listObjectsInfo)
|
||||||
|
|
||||||
|
|||||||
+109
-128
@@ -23,6 +23,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/minio/madmin-go"
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
@@ -74,121 +75,86 @@ func (sys *BucketMetadataSys) Set(bucket string, meta BucketMetadata) {
|
|||||||
|
|
||||||
// Update update bucket metadata for the specified config file.
|
// Update update bucket metadata for the specified config file.
|
||||||
// The configData data should not be modified after being sent here.
|
// The configData data should not be modified after being sent here.
|
||||||
func (sys *BucketMetadataSys) Update(bucket string, configFile string, configData []byte) error {
|
func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configFile string, configData []byte) (updatedAt time.Time, err error) {
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return errServerNotInitialized
|
return updatedAt, errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalIsGateway {
|
if globalIsGateway && globalGatewayName != NASBackendGateway {
|
||||||
// This code is needed only for gateway implementations.
|
if configFile == bucketPolicyConfig {
|
||||||
switch configFile {
|
|
||||||
case bucketSSEConfig:
|
|
||||||
if globalGatewayName == NASBackendGateway {
|
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta.EncryptionConfigXML = configData
|
|
||||||
return meta.Save(GlobalContext, objAPI)
|
|
||||||
}
|
|
||||||
case bucketLifecycleConfig:
|
|
||||||
if globalGatewayName == NASBackendGateway {
|
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta.LifecycleConfigXML = configData
|
|
||||||
return meta.Save(GlobalContext, objAPI)
|
|
||||||
}
|
|
||||||
case bucketTaggingConfig:
|
|
||||||
if globalGatewayName == NASBackendGateway {
|
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta.TaggingConfigXML = configData
|
|
||||||
return meta.Save(GlobalContext, objAPI)
|
|
||||||
}
|
|
||||||
case bucketNotificationConfig:
|
|
||||||
if globalGatewayName == NASBackendGateway {
|
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
meta.NotificationConfigXML = configData
|
|
||||||
return meta.Save(GlobalContext, objAPI)
|
|
||||||
}
|
|
||||||
case bucketPolicyConfig:
|
|
||||||
if configData == nil {
|
if configData == nil {
|
||||||
return objAPI.DeleteBucketPolicy(GlobalContext, bucket)
|
return updatedAt, objAPI.DeleteBucketPolicy(ctx, bucket)
|
||||||
}
|
}
|
||||||
config, err := policy.ParseConfig(bytes.NewReader(configData), bucket)
|
config, err := policy.ParseConfig(bytes.NewReader(configData), bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return updatedAt, err
|
||||||
}
|
}
|
||||||
return objAPI.SetBucketPolicy(GlobalContext, bucket, config)
|
return updatedAt, objAPI.SetBucketPolicy(ctx, bucket, config)
|
||||||
}
|
}
|
||||||
return NotImplemented{}
|
return updatedAt, NotImplemented{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bucket == minioMetaBucket {
|
if bucket == minioMetaBucket {
|
||||||
return errInvalidArgument
|
return updatedAt, errInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
meta, err := loadBucketMetadata(ctx, objAPI, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) {
|
||||||
|
// Only single drive mode needs this fallback.
|
||||||
|
meta = newBucketMetadata(bucket)
|
||||||
|
} else {
|
||||||
|
return updatedAt, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
updatedAt = UTCNow()
|
||||||
switch configFile {
|
switch configFile {
|
||||||
case bucketPolicyConfig:
|
case bucketPolicyConfig:
|
||||||
meta.PolicyConfigJSON = configData
|
meta.PolicyConfigJSON = configData
|
||||||
|
meta.PolicyConfigUpdatedAt = updatedAt
|
||||||
case bucketNotificationConfig:
|
case bucketNotificationConfig:
|
||||||
meta.NotificationConfigXML = configData
|
meta.NotificationConfigXML = configData
|
||||||
case bucketLifecycleConfig:
|
case bucketLifecycleConfig:
|
||||||
meta.LifecycleConfigXML = configData
|
meta.LifecycleConfigXML = configData
|
||||||
case bucketSSEConfig:
|
case bucketSSEConfig:
|
||||||
meta.EncryptionConfigXML = configData
|
meta.EncryptionConfigXML = configData
|
||||||
|
meta.EncryptionConfigUpdatedAt = updatedAt
|
||||||
case bucketTaggingConfig:
|
case bucketTaggingConfig:
|
||||||
meta.TaggingConfigXML = configData
|
meta.TaggingConfigXML = configData
|
||||||
|
meta.TaggingConfigUpdatedAt = updatedAt
|
||||||
case bucketQuotaConfigFile:
|
case bucketQuotaConfigFile:
|
||||||
meta.QuotaConfigJSON = configData
|
meta.QuotaConfigJSON = configData
|
||||||
|
meta.QuotaConfigUpdatedAt = updatedAt
|
||||||
case objectLockConfig:
|
case objectLockConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.ObjectLockConfigXML = configData
|
meta.ObjectLockConfigXML = configData
|
||||||
|
meta.ObjectLockConfigUpdatedAt = updatedAt
|
||||||
case bucketVersioningConfig:
|
case bucketVersioningConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.VersioningConfigXML = configData
|
meta.VersioningConfigXML = configData
|
||||||
|
meta.VersioningConfigUpdatedAt = updatedAt
|
||||||
case bucketReplicationConfig:
|
case bucketReplicationConfig:
|
||||||
if !globalIsErasure && !globalIsDistErasure {
|
|
||||||
return NotImplemented{}
|
|
||||||
}
|
|
||||||
meta.ReplicationConfigXML = configData
|
meta.ReplicationConfigXML = configData
|
||||||
|
meta.ReplicationConfigUpdatedAt = updatedAt
|
||||||
case bucketTargetsFile:
|
case bucketTargetsFile:
|
||||||
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
meta.BucketTargetsConfigJSON, meta.BucketTargetsConfigMetaJSON, err = encryptBucketMetadata(meta.Name, configData, kms.Context{
|
||||||
bucket: meta.Name,
|
bucket: meta.Name,
|
||||||
bucketTargetsFile: bucketTargetsFile,
|
bucketTargetsFile: bucketTargetsFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error encrypting bucket target metadata %w", err)
|
return updatedAt, fmt.Errorf("Error encrypting bucket target metadata %w", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
return updatedAt, fmt.Errorf("Unknown bucket %s metadata update requested %s", bucket, configFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := meta.Save(GlobalContext, objAPI); err != nil {
|
if err := meta.Save(ctx, objAPI); err != nil {
|
||||||
return err
|
return updatedAt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sys.Set(bucket, meta)
|
sys.Set(bucket, meta)
|
||||||
globalNotificationSys.LoadBucketMetadata(GlobalContext, bucket)
|
globalNotificationSys.LoadBucketMetadata(bgContext(ctx), bucket) // Do not use caller context here
|
||||||
|
|
||||||
return nil
|
return updatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get metadata for a bucket.
|
// Get metadata for a bucket.
|
||||||
@@ -219,44 +185,47 @@ func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
|||||||
|
|
||||||
// GetVersioningConfig returns configured versioning config
|
// GetVersioningConfig returns configured versioning config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, error) {
|
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
|
||||||
|
}
|
||||||
|
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, time.Time{}, err
|
||||||
}
|
}
|
||||||
return meta.versioningConfig, nil
|
return meta.versioningConfig, meta.VersioningConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTaggingConfig returns configured tagging config
|
// GetTaggingConfig returns configured tagging config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, error) {
|
func (sys *BucketMetadataSys) GetTaggingConfig(bucket string) (*tags.Tags, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketTaggingNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.taggingConfig == nil {
|
if meta.taggingConfig == nil {
|
||||||
return nil, BucketTaggingNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketTaggingNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.taggingConfig, nil
|
return meta.taggingConfig, meta.TaggingConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetObjectLockConfig returns configured object lock config
|
// GetObjectLockConfig returns configured object lock config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, error) {
|
func (sys *BucketMetadataSys) GetObjectLockConfig(bucket string) (*objectlock.Config, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketObjectLockConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.objectLockConfig == nil {
|
if meta.objectLockConfig == nil {
|
||||||
return nil, BucketObjectLockConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketObjectLockConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.objectLockConfig, nil
|
return meta.objectLockConfig, meta.ObjectLockConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLifecycleConfig returns configured lifecycle config
|
// GetLifecycleConfig returns configured lifecycle config
|
||||||
@@ -278,7 +247,7 @@ func (sys *BucketMetadataSys) GetLifecycleConfig(bucket string) (*lifecycle.Life
|
|||||||
return meta.lifecycleConfig, nil
|
return meta.lifecycleConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
return nil, BucketLifecycleNotFound{Bucket: bucket}
|
||||||
@@ -307,7 +276,7 @@ func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Confi
|
|||||||
return meta.notificationConfig, nil
|
return meta.notificationConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -316,76 +285,92 @@ func (sys *BucketMetadataSys) GetNotificationConfig(bucket string) (*event.Confi
|
|||||||
|
|
||||||
// GetSSEConfig returns configured SSE config
|
// GetSSEConfig returns configured SSE config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, error) {
|
func (sys *BucketMetadataSys) GetSSEConfig(bucket string) (*bucketsse.BucketSSEConfig, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.sseConfig == nil {
|
if meta.sseConfig == nil {
|
||||||
return nil, BucketSSEConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketSSEConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.sseConfig, nil
|
return meta.sseConfig, meta.EncryptionConfigUpdatedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedAt returns the time of creation of bucket
|
||||||
|
func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
||||||
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return meta.Created.UTC(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPolicyConfig returns configured bucket policy
|
// GetPolicyConfig returns configured bucket policy
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) {
|
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
|
||||||
if globalIsGateway {
|
if globalIsGateway {
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return nil, errServerNotInitialized
|
return nil, time.Time{}, errServerNotInitialized
|
||||||
}
|
}
|
||||||
return objAPI.GetBucketPolicy(GlobalContext, bucket)
|
p, err := objAPI.GetBucketPolicy(GlobalContext, bucket)
|
||||||
|
return p, UTCNow(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
if meta.policyConfig == nil {
|
if meta.policyConfig == nil {
|
||||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.policyConfig, nil
|
return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetQuotaConfig returns configured bucket quota
|
// GetQuotaConfig returns configured bucket quota
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetQuotaConfig(bucket string) (*madmin.BucketQuota, error) {
|
func (sys *BucketMetadataSys) GetQuotaConfig(ctx context.Context, bucket string) (*madmin.BucketQuota, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, time.Time{}, BucketQuotaConfigNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
return meta.quotaConfig, nil
|
return meta.quotaConfig, meta.QuotaConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReplicationConfig returns configured bucket replication config
|
// GetReplicationConfig returns configured bucket replication config
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, error) {
|
func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket string) (*replication.Config, time.Time, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(ctx, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errConfigNotFound) {
|
if errors.Is(err, errConfigNotFound) {
|
||||||
return nil, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.replicationConfig == nil {
|
if meta.replicationConfig == nil {
|
||||||
return nil, BucketReplicationConfigNotFound{Bucket: bucket}
|
return nil, time.Time{}, BucketReplicationConfigNotFound{Bucket: bucket}
|
||||||
}
|
}
|
||||||
return meta.replicationConfig, nil
|
return meta.replicationConfig, meta.ReplicationConfigUpdatedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
// GetBucketTargetsConfig returns configured bucket targets for this bucket
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
|
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
|
||||||
meta, err := sys.GetConfig(bucket)
|
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, errConfigNotFound) {
|
||||||
|
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if meta.bucketTargetConfig == nil {
|
if meta.bucketTargetConfig == nil {
|
||||||
@@ -394,23 +379,9 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
|||||||
return meta.bucketTargetConfig, nil
|
return meta.bucketTargetConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketTarget returns the target for the bucket and arn.
|
|
||||||
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
|
|
||||||
targets, err := sys.GetBucketTargetsConfig(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return madmin.BucketTarget{}, err
|
|
||||||
}
|
|
||||||
for _, t := range targets.Targets {
|
|
||||||
if t.Arn == arn {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return madmin.BucketTarget{}, errConfigNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig returns a specific configuration from the bucket metadata.
|
// GetConfig returns a specific configuration from the bucket metadata.
|
||||||
// The returned object may not be modified.
|
// The returned object may not be modified.
|
||||||
func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
|
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
|
||||||
objAPI := newObjectLayerFn()
|
objAPI := newObjectLayerFn()
|
||||||
if objAPI == nil {
|
if objAPI == nil {
|
||||||
return newBucketMetadata(bucket), errServerNotInitialized
|
return newBucketMetadata(bucket), errServerNotInitialized
|
||||||
@@ -430,7 +401,7 @@ func (sys *BucketMetadataSys) GetConfig(bucket string) (BucketMetadata, error) {
|
|||||||
if ok {
|
if ok {
|
||||||
return meta, nil
|
return meta, nil
|
||||||
}
|
}
|
||||||
meta, err := loadBucketMetadata(GlobalContext, objAPI, bucket)
|
meta, err := loadBucketMetadata(ctx, objAPI, bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return meta, err
|
return meta, err
|
||||||
}
|
}
|
||||||
@@ -447,9 +418,9 @@ func (sys *BucketMetadataSys) Init(ctx context.Context, buckets []BucketInfo, ob
|
|||||||
return errServerNotInitialized
|
return errServerNotInitialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// In gateway mode, we don't need to load the policies
|
// In gateway mode, we don't need to load bucket metadata except
|
||||||
// from the backend.
|
// NAS gateway backend.
|
||||||
if globalIsGateway {
|
if globalIsGateway && !objAPI.IsNotificationSupported() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,14 +438,24 @@ func (sys *BucketMetadataSys) concurrentLoad(ctx context.Context, buckets []Buck
|
|||||||
_, _ = objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
|
_, _ = objAPI.HealBucket(ctx, buckets[index].Name, madmin.HealOpts{
|
||||||
// Ensure heal opts for bucket metadata be deep healed all the time.
|
// Ensure heal opts for bucket metadata be deep healed all the time.
|
||||||
ScanMode: madmin.HealDeepScan,
|
ScanMode: madmin.HealDeepScan,
|
||||||
|
Recreate: true,
|
||||||
})
|
})
|
||||||
meta, err := loadBucketMetadata(ctx, objAPI, buckets[index].Name)
|
meta, err := loadBucketMetadata(ctx, objAPI, buckets[index].Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
if !globalIsErasure && !globalIsDistErasure && errors.Is(err, errVolumeNotFound) {
|
||||||
|
meta = newBucketMetadata(buckets[index].Name)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sys.Lock()
|
sys.Lock()
|
||||||
sys.metadataMap[buckets[index].Name] = meta
|
sys.metadataMap[buckets[index].Name] = meta
|
||||||
sys.Unlock()
|
sys.Unlock()
|
||||||
|
|
||||||
|
globalNotificationSys.set(buckets[index], meta) // set notification targets
|
||||||
|
|
||||||
|
globalBucketTargetSys.set(buckets[index], meta) // set remote replication targets
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-9
@@ -81,6 +81,13 @@ type BucketMetadata struct {
|
|||||||
ReplicationConfigXML []byte
|
ReplicationConfigXML []byte
|
||||||
BucketTargetsConfigJSON []byte
|
BucketTargetsConfigJSON []byte
|
||||||
BucketTargetsConfigMetaJSON []byte
|
BucketTargetsConfigMetaJSON []byte
|
||||||
|
PolicyConfigUpdatedAt time.Time
|
||||||
|
ObjectLockConfigUpdatedAt time.Time
|
||||||
|
EncryptionConfigUpdatedAt time.Time
|
||||||
|
TaggingConfigUpdatedAt time.Time
|
||||||
|
QuotaConfigUpdatedAt time.Time
|
||||||
|
ReplicationConfigUpdatedAt time.Time
|
||||||
|
VersioningConfigUpdatedAt time.Time
|
||||||
|
|
||||||
// Unexported fields. Must be updated atomically.
|
// Unexported fields. Must be updated atomically.
|
||||||
policyConfig *policy.Policy
|
policyConfig *policy.Policy
|
||||||
@@ -98,9 +105,10 @@ type BucketMetadata struct {
|
|||||||
|
|
||||||
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
// newBucketMetadata creates BucketMetadata with the supplied name and Created to Now.
|
||||||
func newBucketMetadata(name string) BucketMetadata {
|
func newBucketMetadata(name string) BucketMetadata {
|
||||||
|
now := UTCNow()
|
||||||
return BucketMetadata{
|
return BucketMetadata{
|
||||||
Name: name,
|
Name: name,
|
||||||
Created: UTCNow(),
|
Created: now,
|
||||||
notificationConfig: &event.Config{
|
notificationConfig: &event.Config{
|
||||||
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/",
|
||||||
},
|
},
|
||||||
@@ -120,7 +128,7 @@ func (b *BucketMetadata) Load(ctx context.Context, api ObjectLayer, name string)
|
|||||||
logger.LogIf(ctx, errors.New("bucket name cannot be empty"))
|
logger.LogIf(ctx, errors.New("bucket name cannot be empty"))
|
||||||
return errors.New("bucket name cannot be empty")
|
return errors.New("bucket name cannot be empty")
|
||||||
}
|
}
|
||||||
configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
|
configFile := path.Join(bucketMetaPrefix, name, bucketMetadataFile)
|
||||||
data, err := readConfig(ctx, api, configFile)
|
data, err := readConfig(ctx, api, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -157,8 +165,13 @@ func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket strin
|
|||||||
if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil {
|
if err := b.convertLegacyConfigs(ctx, objectAPI); err != nil {
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate unencrypted remote targets
|
// migrate unencrypted remote targets
|
||||||
return b, b.migrateTargetConfig(ctx, objectAPI)
|
if err = b.migrateTargetConfig(ctx, objectAPI); err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
b.defaultTimestamps()
|
||||||
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAllConfigs will parse all configs and populate the private fields.
|
// parseAllConfigs will parse all configs and populate the private fields.
|
||||||
@@ -277,7 +290,7 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, legacyFile := range legacyConfigs {
|
for _, legacyFile := range legacyConfigs {
|
||||||
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
|
configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
|
||||||
|
|
||||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -338,7 +351,7 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
|
|||||||
}
|
}
|
||||||
|
|
||||||
for legacyFile := range configs {
|
for legacyFile := range configs {
|
||||||
configFile := path.Join(bucketConfigPrefix, b.Name, legacyFile)
|
configFile := path.Join(bucketMetaPrefix, b.Name, legacyFile)
|
||||||
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
|
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
}
|
}
|
||||||
@@ -347,6 +360,37 @@ func (b *BucketMetadata) convertLegacyConfigs(ctx context.Context, objectAPI Obj
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// default timestamps to metadata Created timestamp if unset.
|
||||||
|
func (b *BucketMetadata) defaultTimestamps() {
|
||||||
|
if b.PolicyConfigUpdatedAt.IsZero() {
|
||||||
|
b.PolicyConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.EncryptionConfigUpdatedAt.IsZero() {
|
||||||
|
b.EncryptionConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.TaggingConfigUpdatedAt.IsZero() {
|
||||||
|
b.TaggingConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ObjectLockConfigUpdatedAt.IsZero() {
|
||||||
|
b.ObjectLockConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.QuotaConfigUpdatedAt.IsZero() {
|
||||||
|
b.QuotaConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ReplicationConfigUpdatedAt.IsZero() {
|
||||||
|
b.ReplicationConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.VersioningConfigUpdatedAt.IsZero() {
|
||||||
|
b.VersioningConfigUpdatedAt = b.Created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save config to supplied ObjectLayer api.
|
// Save config to supplied ObjectLayer api.
|
||||||
func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
||||||
if err := b.parseAllConfigs(ctx, api); err != nil {
|
if err := b.parseAllConfigs(ctx, api); err != nil {
|
||||||
@@ -365,7 +409,7 @@ func (b *BucketMetadata) Save(ctx context.Context, api ObjectLayer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
|
configFile := path.Join(bucketMetaPrefix, b.Name, bucketMetadataFile)
|
||||||
return saveConfig(ctx, api, configFile, data)
|
return saveConfig(ctx, api, configFile, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,9 +419,10 @@ func deleteBucketMetadata(ctx context.Context, obj objectDeleter, bucket string)
|
|||||||
metadataFiles := []string{
|
metadataFiles := []string{
|
||||||
dataUsageCacheName,
|
dataUsageCacheName,
|
||||||
bucketMetadataFile,
|
bucketMetadataFile,
|
||||||
|
path.Join(replicationDir, resyncFileName),
|
||||||
}
|
}
|
||||||
for _, metaFile := range metadataFiles {
|
for _, metaFile := range metadataFiles {
|
||||||
configFile := path.Join(bucketConfigPrefix, bucket, metaFile)
|
configFile := path.Join(bucketMetaPrefix, bucket, metaFile)
|
||||||
if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
|
if err := deleteConfig(ctx, obj, configFile); err != nil && err != errConfigNotFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -420,7 +465,7 @@ func encryptBucketMetadata(bucket string, input []byte, kmsContext kms.Context)
|
|||||||
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
|
objectKey := crypto.GenerateKey(key.Plaintext, rand.Reader)
|
||||||
sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "")
|
sealedKey := objectKey.Seal(key.Plaintext, crypto.GenerateIV(rand.Reader), crypto.S3.String(), bucket, "")
|
||||||
crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
|
crypto.S3.CreateMetadata(metadata, key.KeyID, key.Ciphertext, sealedKey)
|
||||||
_, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
_, err = sio.Encrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return output, metabytes, err
|
return output, metabytes, err
|
||||||
}
|
}
|
||||||
@@ -450,6 +495,6 @@ func decryptBucketMetadata(input []byte, bucket string, meta map[string]string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
outbuf := bytes.NewBuffer(nil)
|
outbuf := bytes.NewBuffer(nil)
|
||||||
_, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.CipherSuitesDARE()})
|
_, err = sio.Decrypt(outbuf, bytes.NewBuffer(input), sio.Config{Key: objectKey[:], MinVersion: sio.Version20, CipherSuites: fips.DARECiphers()})
|
||||||
return outbuf.Bytes(), err
|
return outbuf.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|||||||
+180
-5
@@ -108,6 +108,48 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "PolicyConfigUpdatedAt":
|
||||||
|
z.PolicyConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigUpdatedAt":
|
||||||
|
z.ObjectLockConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigUpdatedAt":
|
||||||
|
z.EncryptionConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigUpdatedAt":
|
||||||
|
z.TaggingConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigUpdatedAt":
|
||||||
|
z.QuotaConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ReplicationConfigUpdatedAt":
|
||||||
|
z.ReplicationConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "VersioningConfigUpdatedAt":
|
||||||
|
z.VersioningConfigUpdatedAt, err = dc.ReadTime()
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
err = dc.Skip()
|
err = dc.Skip()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,9 +163,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
// EncodeMsg implements msgp.Encodable
|
||||||
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
||||||
// map header, size 14
|
// map header, size 21
|
||||||
// write "Name"
|
// write "Name"
|
||||||
err = en.Append(0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
err = en.Append(0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -262,15 +304,85 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// write "PolicyConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.PolicyConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "ObjectLockConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.ObjectLockConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "EncryptionConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.EncryptionConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "TaggingConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.TaggingConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "QuotaConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.QuotaConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "ReplicationConfigUpdatedAt"
|
||||||
|
err = en.Append(0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.ReplicationConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// write "VersioningConfigUpdatedAt"
|
||||||
|
err = en.Append(0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = en.WriteTime(z.VersioningConfigUpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
// MarshalMsg implements msgp.Marshaler
|
||||||
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
||||||
o = msgp.Require(b, z.Msgsize())
|
o = msgp.Require(b, z.Msgsize())
|
||||||
// map header, size 14
|
// map header, size 21
|
||||||
// string "Name"
|
// string "Name"
|
||||||
o = append(o, 0x8e, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
o = append(o, 0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||||
o = msgp.AppendString(o, z.Name)
|
o = msgp.AppendString(o, z.Name)
|
||||||
// string "Created"
|
// string "Created"
|
||||||
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||||
@@ -311,6 +423,27 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
// string "BucketTargetsConfigMetaJSON"
|
// string "BucketTargetsConfigMetaJSON"
|
||||||
o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
|
o = append(o, 0xbb, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4d, 0x65, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e)
|
||||||
o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON)
|
o = msgp.AppendBytes(o, z.BucketTargetsConfigMetaJSON)
|
||||||
|
// string "PolicyConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb5, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.PolicyConfigUpdatedAt)
|
||||||
|
// string "ObjectLockConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.ObjectLockConfigUpdatedAt)
|
||||||
|
// string "EncryptionConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.EncryptionConfigUpdatedAt)
|
||||||
|
// string "TaggingConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb6, 0x54, 0x61, 0x67, 0x67, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.TaggingConfigUpdatedAt)
|
||||||
|
// string "QuotaConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb4, 0x51, 0x75, 0x6f, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.QuotaConfigUpdatedAt)
|
||||||
|
// string "ReplicationConfigUpdatedAt"
|
||||||
|
o = append(o, 0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.ReplicationConfigUpdatedAt)
|
||||||
|
// string "VersioningConfigUpdatedAt"
|
||||||
|
o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||||
|
o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +549,48 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
err = msgp.WrapError(err, "BucketTargetsConfigMetaJSON")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "PolicyConfigUpdatedAt":
|
||||||
|
z.PolicyConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PolicyConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ObjectLockConfigUpdatedAt":
|
||||||
|
z.ObjectLockConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ObjectLockConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "EncryptionConfigUpdatedAt":
|
||||||
|
z.EncryptionConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "EncryptionConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "TaggingConfigUpdatedAt":
|
||||||
|
z.TaggingConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "TaggingConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "QuotaConfigUpdatedAt":
|
||||||
|
z.QuotaConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "QuotaConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "ReplicationConfigUpdatedAt":
|
||||||
|
z.ReplicationConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "VersioningConfigUpdatedAt":
|
||||||
|
z.VersioningConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||||
|
return
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
bts, err = msgp.Skip(bts)
|
bts, err = msgp.Skip(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -430,6 +605,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
|
|
||||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||||
func (z *BucketMetadata) Msgsize() (s int) {
|
func (z *BucketMetadata) Msgsize() (s int) {
|
||||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON)
|
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
bucketConfigPrefix = "buckets"
|
|
||||||
bucketNotificationConfig = "notification.xml"
|
bucketNotificationConfig = "notification.xml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,8 +71,8 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.SetRegion(globalServerRegion)
|
config.SetRegion(globalSite.Region)
|
||||||
if err = config.Validate(globalServerRegion, globalNotificationSys.targetList); err != nil {
|
if err = config.Validate(globalSite.Region, globalNotificationSys.targetList); err != nil {
|
||||||
arnErr, ok := err.(*event.ErrARNNotFound)
|
arnErr, ok := err.(*event.ErrARNNotFound)
|
||||||
if ok {
|
if ok {
|
||||||
for i, queue := range config.QueueList {
|
for i, queue := range config.QueueList {
|
||||||
@@ -145,7 +144,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalServerRegion, globalNotificationSys.targetList)
|
config, err := event.ParseConfig(io.LimitReader(r.Body, r.ContentLength), globalSite.Region, globalNotificationSys.targetList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||||
if event.IsEventError(err) {
|
if event.IsEventError(err) {
|
||||||
@@ -161,7 +160,7 @@ func (api objectAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucketName, bucketNotificationConfig, configData); err != nil {
|
if _, err = globalBucketMetadataSys.Update(ctx, bucketName, bucketNotificationConfig, configData); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-25
@@ -44,7 +44,7 @@ func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention,
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
config, _, err := globalBucketMetadataSys.GetObjectLockConfig(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -58,6 +58,10 @@ func (sys *BucketObjectLockSys) Get(bucketName string) (r objectlock.Retention,
|
|||||||
// enforceRetentionForDeletion checks if it is appropriate to remove an
|
// enforceRetentionForDeletion checks if it is appropriate to remove an
|
||||||
// object according to locking configuration when this is lifecycle/ bucket quota asking.
|
// object according to locking configuration when this is lifecycle/ bucket quota asking.
|
||||||
func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
||||||
|
if objInfo.DeleteMarker {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||||
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
||||||
return true
|
return true
|
||||||
@@ -93,7 +97,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
|||||||
if gerr != nil { // error from GetObjectInfo
|
if gerr != nil { // error from GetObjectInfo
|
||||||
switch gerr.(type) {
|
switch gerr.(type) {
|
||||||
case MethodNotAllowed: // This happens usually for a delete marker
|
case MethodNotAllowed: // This happens usually for a delete marker
|
||||||
if oi.DeleteMarker {
|
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||||
// Delete marker should be present and valid.
|
// Delete marker should be present and valid.
|
||||||
return ErrNone
|
return ErrNone
|
||||||
}
|
}
|
||||||
@@ -175,68 +179,76 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
|
|||||||
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
|
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
|
||||||
// governance bypass headers are set and user has governance bypass permissions.
|
// governance bypass headers are set and user has governance bypass permissions.
|
||||||
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
|
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
|
||||||
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) (ObjectInfo, APIErrorCode) {
|
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, oi ObjectInfo, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) error {
|
||||||
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||||
opts, err := getOpts(ctx, r, bucket, object)
|
|
||||||
if err != nil {
|
|
||||||
return ObjectInfo{}, toAPIErrorCode(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oi, err := getObjectInfoFn(ctx, bucket, object, opts)
|
|
||||||
if err != nil {
|
|
||||||
return oi, toAPIErrorCode(ctx, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t, err := objectlock.UTCNowNTP()
|
t, err := objectlock.UTCNowNTP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.LogIf(ctx, err)
|
logger.LogIf(ctx, err)
|
||||||
return oi, ErrObjectLocked
|
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass in relative days from current time, to additionally to verify "object-lock-remaining-retention-days" policy if any.
|
// Pass in relative days from current time, to additionally
|
||||||
|
// to verify "object-lock-remaining-retention-days" policy if any.
|
||||||
days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
|
days := int(math.Ceil(math.Abs(objRetention.RetainUntilDate.Sub(t).Hours()) / 24))
|
||||||
|
|
||||||
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
ret := objectlock.GetObjectRetentionMeta(oi.UserDefined)
|
||||||
if ret.Mode.Valid() {
|
if ret.Mode.Valid() {
|
||||||
// Retention has expired you may change whatever you like.
|
// Retention has expired you may change whatever you like.
|
||||||
if ret.RetainUntilDate.Before(t) {
|
if ret.RetainUntilDate.Before(t) {
|
||||||
perm := isPutRetentionAllowed(bucket, object,
|
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||||
days, objRetention.RetainUntilDate.Time,
|
days, objRetention.RetainUntilDate.Time,
|
||||||
objRetention.Mode, byPassSet, r, cred,
|
objRetention.Mode, byPassSet, r, cred,
|
||||||
owner)
|
owner)
|
||||||
return oi, perm
|
switch apiErr {
|
||||||
|
case ErrAccessDenied:
|
||||||
|
return errAuthentication
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ret.Mode {
|
switch ret.Mode {
|
||||||
case objectlock.RetGovernance:
|
case objectlock.RetGovernance:
|
||||||
govPerm := isPutRetentionAllowed(bucket, object, days,
|
govPerm := isPutRetentionAllowed(oi.Bucket, oi.Name, days,
|
||||||
objRetention.RetainUntilDate.Time, objRetention.Mode,
|
objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||||
byPassSet, r, cred, owner)
|
byPassSet, r, cred, owner)
|
||||||
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
|
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
|
||||||
if !byPassSet {
|
if !byPassSet {
|
||||||
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
||||||
return oi, ErrObjectLocked
|
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return oi, govPerm
|
switch govPerm {
|
||||||
|
case ErrAccessDenied:
|
||||||
|
return errAuthentication
|
||||||
|
}
|
||||||
|
return nil
|
||||||
case objectlock.RetCompliance:
|
case objectlock.RetCompliance:
|
||||||
// Compliance retention mode cannot be changed or shortened.
|
// Compliance retention mode cannot be changed or shortened.
|
||||||
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/object-lock-overview.html#object-lock-retention-modes
|
||||||
if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
if objRetention.Mode != objectlock.RetCompliance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
|
||||||
return oi, ErrObjectLocked
|
return ObjectLocked{Bucket: oi.Bucket, Object: oi.Name, VersionID: oi.VersionID}
|
||||||
}
|
}
|
||||||
compliancePerm := isPutRetentionAllowed(bucket, object,
|
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||||
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
|
||||||
false, r, cred, owner)
|
false, r, cred, owner)
|
||||||
return oi, compliancePerm
|
switch apiErr {
|
||||||
|
case ErrAccessDenied:
|
||||||
|
return errAuthentication
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return oi, ErrNone
|
return nil
|
||||||
} // No pre-existing retention metadata present.
|
} // No pre-existing retention metadata present.
|
||||||
|
|
||||||
perm := isPutRetentionAllowed(bucket, object,
|
apiErr := isPutRetentionAllowed(oi.Bucket, oi.Name,
|
||||||
days, objRetention.RetainUntilDate.Time,
|
days, objRetention.RetainUntilDate.Time,
|
||||||
objRetention.Mode, byPassSet, r, cred, owner)
|
objRetention.Mode, byPassSet, r, cred, owner)
|
||||||
return oi, perm
|
switch apiErr {
|
||||||
|
case ErrAccessDenied:
|
||||||
|
return errAuthentication
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPutObjectLockAllowed enforces object retention policy and legal hold policy
|
// checkPutObjectLockAllowed enforces object retention policy and legal hold policy
|
||||||
|
|||||||
@@ -18,12 +18,15 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/bucket/policy"
|
"github.com/minio/pkg/bucket/policy"
|
||||||
)
|
)
|
||||||
@@ -76,7 +79,13 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketPolicy, err := policy.ParseConfig(io.LimitReader(r.Body, r.ContentLength), bucket)
|
bucketPolicyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, r.ContentLength))
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketPolicy, err := policy.ParseConfig(bytes.NewReader(bucketPolicyBytes), bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -94,7 +103,19 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = globalBucketMetadataSys.Update(bucket, bucketPolicyConfig, configData); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, configData)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypePolicy,
|
||||||
|
Bucket: bucket,
|
||||||
|
Policy: bucketPolicyBytes,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -129,7 +150,18 @@ func (api objectAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := globalBucketMetadataSys.Update(bucket, bucketPolicyConfig, nil); err != nil {
|
updatedAt, err := globalBucketMetadataSys.Update(ctx, bucket, bucketPolicyConfig, nil)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call site replication hook.
|
||||||
|
if err := globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||||
|
Type: madmin.SRBucketMetaTypePolicy,
|
||||||
|
Bucket: bucket,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,60 +37,72 @@ import (
|
|||||||
func getAnonReadOnlyBucketPolicy(bucketName string) *policy.Policy {
|
func getAnonReadOnlyBucketPolicy(bucketName string) *policy.Policy {
|
||||||
return &policy.Policy{
|
return &policy.Policy{
|
||||||
Version: policy.DefaultVersion,
|
Version: policy.DefaultVersion,
|
||||||
Statements: []policy.Statement{policy.NewStatement(
|
Statements: []policy.Statement{
|
||||||
policy.Allow,
|
policy.NewStatement(
|
||||||
policy.NewPrincipal("*"),
|
"",
|
||||||
policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
|
policy.Allow,
|
||||||
policy.NewResourceSet(policy.NewResource(bucketName, "")),
|
policy.NewPrincipal("*"),
|
||||||
condition.NewFunctions(),
|
policy.NewActionSet(policy.GetBucketLocationAction, policy.ListBucketAction),
|
||||||
)},
|
policy.NewResourceSet(policy.NewResource(bucketName, "")),
|
||||||
|
condition.NewFunctions(),
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAnonWriteOnlyBucketPolicy(bucketName string) *policy.Policy {
|
func getAnonWriteOnlyBucketPolicy(bucketName string) *policy.Policy {
|
||||||
return &policy.Policy{
|
return &policy.Policy{
|
||||||
Version: policy.DefaultVersion,
|
Version: policy.DefaultVersion,
|
||||||
Statements: []policy.Statement{policy.NewStatement(
|
Statements: []policy.Statement{
|
||||||
policy.Allow,
|
policy.NewStatement(
|
||||||
policy.NewPrincipal("*"),
|
"",
|
||||||
policy.NewActionSet(
|
policy.Allow,
|
||||||
policy.GetBucketLocationAction,
|
policy.NewPrincipal("*"),
|
||||||
policy.ListBucketMultipartUploadsAction,
|
policy.NewActionSet(
|
||||||
|
policy.GetBucketLocationAction,
|
||||||
|
policy.ListBucketMultipartUploadsAction,
|
||||||
|
),
|
||||||
|
policy.NewResourceSet(policy.NewResource(bucketName, "")),
|
||||||
|
condition.NewFunctions(),
|
||||||
),
|
),
|
||||||
policy.NewResourceSet(policy.NewResource(bucketName, "")),
|
},
|
||||||
condition.NewFunctions(),
|
|
||||||
)},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAnonReadOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
|
func getAnonReadOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
|
||||||
return &policy.Policy{
|
return &policy.Policy{
|
||||||
Version: policy.DefaultVersion,
|
Version: policy.DefaultVersion,
|
||||||
Statements: []policy.Statement{policy.NewStatement(
|
Statements: []policy.Statement{
|
||||||
policy.Allow,
|
policy.NewStatement(
|
||||||
policy.NewPrincipal("*"),
|
"",
|
||||||
policy.NewActionSet(policy.GetObjectAction),
|
policy.Allow,
|
||||||
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
|
policy.NewPrincipal("*"),
|
||||||
condition.NewFunctions(),
|
policy.NewActionSet(policy.GetObjectAction),
|
||||||
)},
|
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
|
||||||
|
condition.NewFunctions(),
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
|
func getAnonWriteOnlyObjectPolicy(bucketName, prefix string) *policy.Policy {
|
||||||
return &policy.Policy{
|
return &policy.Policy{
|
||||||
Version: policy.DefaultVersion,
|
Version: policy.DefaultVersion,
|
||||||
Statements: []policy.Statement{policy.NewStatement(
|
Statements: []policy.Statement{
|
||||||
policy.Allow,
|
policy.NewStatement(
|
||||||
policy.NewPrincipal("*"),
|
"",
|
||||||
policy.NewActionSet(
|
policy.Allow,
|
||||||
policy.AbortMultipartUploadAction,
|
policy.NewPrincipal("*"),
|
||||||
policy.DeleteObjectAction,
|
policy.NewActionSet(
|
||||||
policy.ListMultipartUploadPartsAction,
|
policy.AbortMultipartUploadAction,
|
||||||
policy.PutObjectAction,
|
policy.DeleteObjectAction,
|
||||||
|
policy.ListMultipartUploadPartsAction,
|
||||||
|
policy.PutObjectAction,
|
||||||
|
),
|
||||||
|
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
|
||||||
|
condition.NewFunctions(),
|
||||||
),
|
),
|
||||||
policy.NewResourceSet(policy.NewResource(bucketName, prefix)),
|
},
|
||||||
condition.NewFunctions(),
|
|
||||||
)},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,11 +113,12 @@ func TestCreateBucket(t *testing.T) {
|
|||||||
|
|
||||||
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
// testCreateBucket - Test for calling Create Bucket and ensure we get one and only one success.
|
||||||
func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testCreateBucket(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||||
|
|
||||||
const n = 100
|
const n = 100
|
||||||
var start = make(chan struct{})
|
start := make(chan struct{})
|
||||||
var ok, errs int
|
var ok, errs int
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
@@ -147,8 +160,8 @@ func TestPutBucketPolicyHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
// testPutBucketPolicyHandler - Test for Bucket policy end point.
|
||||||
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
bucketName1 := fmt.Sprintf("%s-1", bucketName)
|
||||||
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
|
if err := obj.MakeBucketWithLocation(GlobalContext, bucketName1, BucketOptions{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -333,7 +346,6 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
// create unsigned HTTP request for PutBucketPolicyHandler.
|
// create unsigned HTTP request for PutBucketPolicyHandler.
|
||||||
anonReq, err := newTestRequest(http.MethodPut, getPutPolicyURL("", bucketName),
|
anonReq, err := newTestRequest(http.MethodPut, getPutPolicyURL("", bucketName),
|
||||||
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)))
|
int64(len(bucketPolicyStr)), bytes.NewReader([]byte(bucketPolicyStr)))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
||||||
instanceType, bucketName, err)
|
instanceType, bucketName, err)
|
||||||
@@ -352,14 +364,12 @@ func testPutBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4(http.MethodPut, getPutPolicyURL("", nilBucket),
|
||||||
0, nil, "", "", nil)
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
// execute the object layer set to `nil` test.
|
// execute the object layer set to `nil` test.
|
||||||
// `ExecObjectLayerAPINilTest` manages the operation.
|
// `ExecObjectLayerAPINilTest` manages the operation.
|
||||||
ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
|
ExecObjectLayerAPINilTest(t, nilBucket, "", instanceType, apiRouter, nilReq)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper for calling Get Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
// Wrapper for calling Get Bucket Policy HTTP handler tests for both Erasure multiple disks and single node setup.
|
||||||
@@ -369,7 +379,8 @@ func TestGetBucketPolicyHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
// testGetBucketPolicyHandler - Test for end point which fetches the access policy json of the given bucket.
|
||||||
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
// template for constructing HTTP request body for PUT bucket policy.
|
||||||
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
bucketPolicyTemplate := `{"Version":"2012-10-17","Statement":[{"Action":["s3:GetBucketLocation","s3:ListBucket"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s"]},{"Action":["s3:GetObject"],"Effect":"Allow","Principal":{"AWS":["*"]},"Resource":["arn:aws:s3:::%s/this*"]}]}`
|
||||||
|
|
||||||
@@ -465,7 +476,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
// construct HTTP request for PUT bucket policy endpoint.
|
// construct HTTP request for PUT bucket policy endpoint.
|
||||||
reqV4, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", testCase.bucketName),
|
reqV4, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", testCase.bucketName),
|
||||||
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
0, nil, testCase.accessKey, testCase.secretKey, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
t.Fatalf("Test %d: Failed to create HTTP request for GetBucketPolicyHandler: <ERROR> %v", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -540,7 +550,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
// Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
|
// Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
|
||||||
// create unsigned HTTP request for PutBucketPolicyHandler.
|
// create unsigned HTTP request for PutBucketPolicyHandler.
|
||||||
anonReq, err := newTestRequest(http.MethodGet, getPutPolicyURL("", bucketName), 0, nil)
|
anonReq, err := newTestRequest(http.MethodGet, getPutPolicyURL("", bucketName), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
||||||
instanceType, bucketName, err)
|
instanceType, bucketName, err)
|
||||||
@@ -559,7 +568,6 @@ func testGetBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string
|
|||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4(http.MethodGet, getGetPolicyURL("", nilBucket),
|
||||||
0, nil, "", "", nil)
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
@@ -575,8 +583,8 @@ func TestDeleteBucketPolicyHandler(t *testing.T) {
|
|||||||
|
|
||||||
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
// testDeleteBucketPolicyHandler - Test for Delete bucket policy end point.
|
||||||
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||||
credentials auth.Credentials, t *testing.T) {
|
credentials auth.Credentials, t *testing.T,
|
||||||
|
) {
|
||||||
// template for constructing HTTP request body for PUT bucket policy.
|
// template for constructing HTTP request body for PUT bucket policy.
|
||||||
bucketPolicyTemplate := `{
|
bucketPolicyTemplate := `{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
@@ -743,7 +751,6 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
// Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
|
// Bucket policy related functions doesn't support anonymous requests, setting policies shouldn't make a difference.
|
||||||
// create unsigned HTTP request for PutBucketPolicyHandler.
|
// create unsigned HTTP request for PutBucketPolicyHandler.
|
||||||
anonReq, err := newTestRequest(http.MethodDelete, getPutPolicyURL("", bucketName), 0, nil)
|
anonReq, err := newTestRequest(http.MethodDelete, getPutPolicyURL("", bucketName), 0, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
t.Fatalf("MinIO %s: Failed to create an anonymous request for bucket \"%s\": <ERROR> %v",
|
||||||
instanceType, bucketName, err)
|
instanceType, bucketName, err)
|
||||||
@@ -762,7 +769,6 @@ func testDeleteBucketPolicyHandler(obj ObjectLayer, instanceType, bucketName str
|
|||||||
|
|
||||||
nilReq, err := newTestSignedRequestV4(http.MethodDelete, getDeletePolicyURL("", nilBucket),
|
nilReq, err := newTestSignedRequestV4(http.MethodDelete, getDeletePolicyURL("", nilBucket),
|
||||||
0, nil, "", "", nil)
|
0, nil, "", "", nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
t.Errorf("MinIO %s: Failed to create HTTP request for testing the response when object Layer is set to `nil`.", instanceType)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user