mirror of
https://github.com/pgsty/minio.git
synced 2026-10-19 07:56:09 +03:00
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e20aab25ec | |||
| 66bea3942a | |||
| 08acd9c43d | |||
| ff5988f4e0 | |||
| 1bf23374a3 | |||
| 899b429094 | |||
| c47ff44f5e | |||
| 8af0773baf | |||
| 37cbd114de | |||
| 2dbb1cff4a | |||
| 6efcf9c982 | |||
| 0bc34952eb | |||
| f6b48ed02a | |||
| e37c4efc6e | |||
| 22f5bc643c | |||
| 15fd5ce2fa | |||
| 7f782983ca | |||
| 9d628346eb | |||
| bde533a9c7 | |||
| 2fcb75d86d | |||
| aae6846413 | |||
| 5317a0b755 | |||
| 73de721a63 | |||
| d2f5c3621f | |||
| 1818764840 | |||
| d3e5e607a7 | |||
| c1943ea3af | |||
| 2a82c15bf1 | |||
| 87b6fb37d6 | |||
| 21fbe88e1f | |||
| 1f8b9b4bd5 | |||
| fcbed41cc3 | |||
| 216069d0da | |||
| eefa047974 | |||
| d8dad5c9ea | |||
| bf8a68879c | |||
| f3248a4b37 | |||
| d315d012a4 | |||
| bd9bf3693f | |||
| 15daa2e74a | |||
| 74759b05a5 | |||
| 82ce78a17c | |||
| 9af6c6ceef | |||
| 021372cc4c | |||
| b94ab07c2f | |||
| 7605d07bb2 | |||
| ccc5801112 | |||
| 7c72b25ef0 | |||
| 02c2ec3027 | |||
| 65c31fab12 | |||
| b6b68be052 | |||
| 15911c85f6 | |||
| 5a1612fe32 | |||
| bbb7ae156c | |||
| f9b8d1c699 | |||
| 1443b5927a | |||
| 35ef35b5c1 | |||
| 6806537eb3 | |||
| 64de61d15d | |||
| 22b7c8cd8a | |||
| c4d0c49a5f | |||
| 142a5b0dcd | |||
| 25db1e4eca | |||
| 47a48b6832 | |||
| e98309eb75 | |||
| 87051872a7 | |||
| a2aed12dcd | |||
| d8e6e76e89 | |||
| 8c33fdf5f4 | |||
| ad4e511026 | |||
| 4a562d6732 | |||
| a9082e4f79 | |||
| 6278679ffd | |||
| 0474791cf8 | |||
| 69f819e199 | |||
| f32efd5429 | |||
| 22c247a988 | |||
| 35d71682f6 | |||
| 3d6b88a60e | |||
| 26a0803388 | |||
| ae95384dd8 | |||
| 0f0dcf0c5e | |||
| 6f2406b0b6 | |||
| bb24346e04 | |||
| be45ffd8a4 | |||
| f986b0c493 | |||
| c9e87f0548 | |||
| 43468f4d47 | |||
| 91987d6f7a |
@@ -18,9 +18,12 @@ cd .github/workflows/mint
|
||||
docker-compose -f minio-${MODE}.yaml up -d
|
||||
sleep 5m
|
||||
|
||||
docker run --rm --net=host \
|
||||
# Stop two nodes, one of each pool, to check that all S3 calls work while quorum is still there
|
||||
[ "${MODE}" == "pools" ] && docker-compose -f minio-${MODE}.yaml stop minio{2,6}
|
||||
|
||||
docker run --rm --net=mint_default \
|
||||
--name="mint-${MODE}-${JOB_NAME}" \
|
||||
-e SERVER_ENDPOINT="127.0.0.1:9000" \
|
||||
-e SERVER_ENDPOINT="nginx:9000" \
|
||||
-e ACCESS_KEY="${ACCESS_KEY}" \
|
||||
-e SECRET_KEY="${SECRET_KEY}" \
|
||||
-e ENABLE_HTTPS=0 \
|
||||
|
||||
@@ -2465,6 +2465,39 @@ SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/alexbrainman/sspi
|
||||
https://github.com/alexbrainman/sspi
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/apache/thrift
|
||||
https://github.com/apache/thrift
|
||||
----------------------------------------------------------------
|
||||
@@ -3027,7 +3060,7 @@ https://github.com/charmbracelet/bubbles
|
||||
----------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Charmbracelet, Inc
|
||||
Copyright (c) 2020-2023 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -9210,214 +9243,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/google/martian
|
||||
https://github.com/google/martian
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/google/martian/v3
|
||||
https://github.com/google/martian/v3
|
||||
----------------------------------------------------------------
|
||||
@@ -16197,6 +16022,34 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/dnscache
|
||||
https://github.com/minio/dnscache
|
||||
----------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 MinIO, Inc.
|
||||
Copyright (c) 2018 Olivier Poitrey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/dperf
|
||||
https://github.com/minio/dperf
|
||||
----------------------------------------------------------------
|
||||
@@ -17739,8 +17592,8 @@ For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/madmin-go/v2
|
||||
https://github.com/minio/madmin-go/v2
|
||||
github.com/minio/madmin-go/v3
|
||||
https://github.com/minio/madmin-go/v3
|
||||
----------------------------------------------------------------
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
@@ -25339,33 +25192,6 @@ THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/rs/dnscache
|
||||
https://github.com/rs/dnscache
|
||||
----------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Olivier Poitrey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/rs/xid
|
||||
https://github.com/rs/xid
|
||||
----------------------------------------------------------------
|
||||
@@ -29247,6 +29073,422 @@ https://google.golang.org/genproto
|
||||
|
||||
================================================================
|
||||
|
||||
google.golang.org/genproto/googleapis/api
|
||||
https://google.golang.org/genproto/googleapis/api
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================
|
||||
|
||||
google.golang.org/genproto/googleapis/rpc
|
||||
https://google.golang.org/genproto/googleapis/rpc
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================
|
||||
|
||||
google.golang.org/grpc
|
||||
https://google.golang.org/grpc
|
||||
----------------------------------------------------------------
|
||||
|
||||
@@ -36,7 +36,6 @@ RUN \
|
||||
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 && \
|
||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc -o /opt/bin/mc && \
|
||||
gzip /opt/bin/mc && \
|
||||
microdnf clean all && \
|
||||
chmod +x /opt/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh && \
|
||||
|
||||
@@ -74,6 +74,9 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
|
||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||
@MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
|
||||
test-sio-error:
|
||||
@(env bash $(PWD)/docs/bucket/replication/sio-error.sh)
|
||||
|
||||
test-replication-2site:
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||
|
||||
@@ -83,7 +86,7 @@ test-replication-3site:
|
||||
test-delete-replication:
|
||||
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
|
||||
|
||||
test-replication: install test-replication-2site test-replication-3site test-delete-replication ## verify multi site replication
|
||||
test-replication: install test-replication-2site test-replication-3site test-delete-replication test-sio-error ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
|
||||
test-site-replication-ldap: install ## verify automatic site replication
|
||||
|
||||
@@ -9,7 +9,7 @@ function _init() {
|
||||
export CGO_ENABLED=0
|
||||
|
||||
## List of architectures and OS to test coss compilation.
|
||||
SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/amd64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64 linux/mips openbsd/amd64"
|
||||
SUPPORTED_OSARCH="linux/ppc64le linux/mips64 linux/amd64 linux/arm64 linux/s390x darwin/arm64 darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386 netbsd/amd64 linux/mips"
|
||||
}
|
||||
|
||||
function _build() {
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/kes-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/kes-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
@@ -241,6 +241,8 @@ func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (re
|
||||
// 1. `subsys:target` -> request for config of a single subsystem and target pair.
|
||||
// 2. `subsys:` -> request for config of a single subsystem and the default target.
|
||||
// 3. `subsys` -> request for config of all targets for the given subsystem.
|
||||
//
|
||||
// This is a reporting API and config secrets are redacted in the response.
|
||||
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigKV")
|
||||
|
||||
@@ -268,7 +270,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
}
|
||||
|
||||
subSysConfigs, err := cfg.GetSubsysInfo(subSys, target)
|
||||
subSysConfigs, err := cfg.GetSubsysInfo(subSys, target, true)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -490,7 +492,9 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// GetConfigHandler - GET /minio/admin/v3/config
|
||||
// Get config.json of this minio setup.
|
||||
//
|
||||
// This endpoint is mainly for exporting and backing up the configuration.
|
||||
// Secrets are not redacted.
|
||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfig")
|
||||
|
||||
@@ -507,7 +511,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
hkvs := config.HelpSubSysMap[""]
|
||||
for _, hkv := range hkvs {
|
||||
// We ignore the error below, as we cannot get one.
|
||||
cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "")
|
||||
cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "", false)
|
||||
|
||||
for _, item := range cfgSubsysItems {
|
||||
off := item.Config.Get(config.Enable) == config.EnableOff
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/config"
|
||||
cfgldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
@@ -206,9 +206,9 @@ func (a adminAPIHandlers) AddIdentityProviderCfg(w http.ResponseWriter, r *http.
|
||||
|
||||
// UpdateIdentityProviderCfg: updates an existing IDP config for openid/ldap.
|
||||
//
|
||||
// PATCH <admin-prefix>/idp-cfg/openid/dex1 -> update named config `dex1`
|
||||
// POST <admin-prefix>/idp-cfg/openid/dex1 -> update named config `dex1`
|
||||
//
|
||||
// PATCH <admin-prefix>/idp-cfg/openid/_ -> update (default) named config `_`
|
||||
// POST <admin-prefix>/idp-cfg/openid/_ -> update (default) named config `_`
|
||||
func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "UpdateIdentityProviderCfg")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
@@ -150,7 +150,7 @@ func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Call IAM subsystem
|
||||
updatedAt, addedOrRemoved, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
|
||||
updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
|
||||
@@ -95,7 +95,7 @@ func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Reque
|
||||
poolIndices = append(poolIndices, idx)
|
||||
}
|
||||
|
||||
if len(poolIndices) > 0 && globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal {
|
||||
if len(poolIndices) > 0 && !globalEndpoints[poolIndices[0]].Endpoints[0].IsLocal {
|
||||
ep := globalEndpoints[poolIndices[0]].Endpoints[0]
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Endpoint.Host == ep.Host {
|
||||
|
||||
@@ -20,16 +20,19 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/mux"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
@@ -543,3 +546,44 @@ func (a adminAPIHandlers) SiteReplicationResyncOp(w http.ResponseWriter, r *http
|
||||
}
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
// SiteReplicationDevNull - everything goes to io.Discard
|
||||
// [POST] /minio/admin/v3/site-replication/devnull
|
||||
func (a *adminAPIHandlers) SiteReplicationDevNull(w http.ResponseWriter, r *http.Request) {
|
||||
globalSiteNetPerfRX.Connect()
|
||||
defer globalSiteNetPerfRX.Disconnect()
|
||||
|
||||
connectTime := time.Now()
|
||||
ctx := newContext(r, w, "SiteReplicationDevNull")
|
||||
for {
|
||||
n, err := io.CopyN(io.Discard, r.Body, 128*humanize.KiByte)
|
||||
atomic.AddUint64(&globalSiteNetPerfRX.RX, uint64(n))
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
// If there is a disconnection before globalNetPerfMinDuration (we give a margin of error of 1 sec)
|
||||
// would mean the network is not stable. Logging here will help in debugging network issues.
|
||||
if time.Since(connectTime) < (globalNetPerfMinDuration - time.Second) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationNetPerf - everything goes to io.Discard
|
||||
// [POST] /minio/admin/v3/site-replication/netperf
|
||||
func (a *adminAPIHandlers) SiteReplicationNetPerf(w http.ResponseWriter, r *http.Request) {
|
||||
durationStr := r.Form.Get(peerRESTDuration)
|
||||
duration, _ := time.ParseDuration(durationStr)
|
||||
if duration < globalNetPerfMinDuration {
|
||||
duration = globalNetPerfMinDuration
|
||||
}
|
||||
result := siteNetperf(r.Context(), duration)
|
||||
logger.LogIf(r.Context(), gob.NewEncoder(w).Encode(result))
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/pkg/sync/errgroup"
|
||||
)
|
||||
|
||||
+35
-213
@@ -26,11 +26,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -1771,24 +1770,40 @@ func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *ht
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// AttachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/attach
|
||||
func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
||||
// AttachDetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/{operation}
|
||||
func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AttachPolicyBuiltin")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.UpdatePolicyAssociationAction,
|
||||
iampolicy.AttachPolicyAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
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
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
operation := mux.Vars(r)["operation"]
|
||||
if operation != "attach" && operation != "detach" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
isAttach := operation == "attach"
|
||||
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -1806,235 +1821,42 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
userOrGroup := par.User
|
||||
isGroup := false
|
||||
|
||||
if userOrGroup == "" {
|
||||
userOrGroup = par.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
if isGroup {
|
||||
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ok, _, err := globalIAMSys.IsTempUser(userOrGroup)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// When the user is root credential you are not allowed to
|
||||
// add policies for root user.
|
||||
if userOrGroup == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that user exists.
|
||||
_, ok = globalIAMSys.GetUser(ctx, userOrGroup)
|
||||
if !ok {
|
||||
updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateBuiltin(ctx, isAttach, par)
|
||||
if err != nil {
|
||||
if err == errNoSuchUser || err == errNoSuchGroup {
|
||||
if globalIAMSys.LDAPConfig.Enabled() {
|
||||
// When LDAP is enabled, warn user that they are using the wrong
|
||||
// API.
|
||||
// API. FIXME: error can be no such group as well - fix errNoSuchUserLDAPWarn
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var existingPolicies []string
|
||||
if isGroup {
|
||||
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
|
||||
} else {
|
||||
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyMap := make(map[string]bool)
|
||||
for _, p := range existingPolicies {
|
||||
policyMap[p] = true
|
||||
}
|
||||
|
||||
policiesToAttach := par.Policies
|
||||
|
||||
// Check if policy is already attached to user.
|
||||
for _, p := range policiesToAttach {
|
||||
if _, ok := policyMap[p]; ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyAlreadyAttached), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
existingPolicies = append(existingPolicies, policiesToAttach...)
|
||||
newPolicies := strings.Join(existingPolicies, ",")
|
||||
|
||||
userType := regUser
|
||||
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(policiesToAttach, ","),
|
||||
},
|
||||
respBody := madmin.PolicyAssociationResp{
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
writeResponse(w, http.StatusCreated, nil, mimeNone)
|
||||
}
|
||||
|
||||
// DetachPolicyBuiltin - POST /minio/admin/v3/idp/builtin/policy/detach
|
||||
func (a adminAPIHandlers) DetachPolicyBuiltin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DetachPolicyBuiltin")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AttachPolicyAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
if err = json.Unmarshal(reqBytes, &par); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userOrGroup := par.User
|
||||
isGroup := false
|
||||
|
||||
if userOrGroup == "" {
|
||||
userOrGroup = par.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
if isGroup {
|
||||
_, err := globalIAMSys.GetGroupDescription(userOrGroup)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if isAttach {
|
||||
respBody.PoliciesAttached = addedOrRemoved
|
||||
} else {
|
||||
ok, _, err := globalIAMSys.IsTempUser(userOrGroup)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that user exists.
|
||||
_, ok = globalIAMSys.GetUser(ctx, userOrGroup)
|
||||
if !ok {
|
||||
if globalIAMSys.LDAPConfig.Enabled() {
|
||||
// When LDAP is enabled, warn user that they are using the wrong
|
||||
// API.
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUserLDAPWarn), r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
||||
return
|
||||
}
|
||||
respBody.PoliciesDetached = addedOrRemoved
|
||||
}
|
||||
|
||||
userType := regUser
|
||||
if globalIAMSys.GetUsersSysType() == LDAPUsersSysType {
|
||||
userType = stsUser
|
||||
}
|
||||
|
||||
var existingPolicies []string
|
||||
if isGroup {
|
||||
existingPolicies, err = globalIAMSys.PolicyDBGet(userOrGroup, true)
|
||||
} else {
|
||||
existingPolicies, err = globalIAMSys.GetUserPolicies(userOrGroup)
|
||||
}
|
||||
data, err := json.Marshal(respBody)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
policyMap := make(map[string]bool)
|
||||
for _, p := range existingPolicies {
|
||||
policyMap[p] = true
|
||||
}
|
||||
|
||||
policiesToDetach := par.Policies
|
||||
|
||||
// Check if policy is already attached to user.
|
||||
for _, p := range policiesToDetach {
|
||||
if _, ok := policyMap[p]; ok {
|
||||
delete(policyMap, p)
|
||||
} else {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyNotAttached), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
newPoliciesSl := []string{}
|
||||
for p := range policyMap {
|
||||
newPoliciesSl = append(newPoliciesSl, p)
|
||||
}
|
||||
|
||||
newPolicies := strings.Join(newPoliciesSl, ",")
|
||||
|
||||
updatedAt, err := globalIAMSys.PolicyDBSet(ctx, userOrGroup, newPolicies, userType, isGroup)
|
||||
encryptedData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: newPolicies,
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
// Return successful JSON response
|
||||
writeSuccessNoContent(w)
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
const (
|
||||
|
||||
@@ -33,10 +33,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
cr "github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/signer"
|
||||
@@ -877,7 +876,7 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByUser(c *check) {
|
||||
|
||||
// Create an madmin client with user creds
|
||||
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
|
||||
Creds: cr.NewStaticV4(accessKey, secretKey, ""),
|
||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: s.secure,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1072,7 +1071,7 @@ func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
|
||||
|
||||
// Create an madmin client with user creds
|
||||
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
|
||||
Creds: cr.NewStaticV4(accessKey, secretKey, ""),
|
||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: s.secure,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+74
-4
@@ -45,8 +45,8 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v2/estream"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/madmin-go/v3/estream"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/dsync"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
@@ -1153,6 +1153,54 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
}
|
||||
|
||||
// SitePerfHandler - measures network throughput between site replicated setups
|
||||
func (a adminAPIHandlers) SitePerfHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SitePerfHandler")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !globalSiteReplicationSys.isEnabled() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "site-net-perf")
|
||||
lkctx, err := nsLock.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
|
||||
return
|
||||
}
|
||||
defer nsLock.Unlock(lkctx)
|
||||
|
||||
durationStr := r.Form.Get(peerRESTDuration)
|
||||
duration, err := time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
duration = globalNetPerfMinDuration
|
||||
}
|
||||
|
||||
if duration < globalNetPerfMinDuration {
|
||||
// We need sample size of minimum 10 secs.
|
||||
duration = globalNetPerfMinDuration
|
||||
}
|
||||
|
||||
duration = duration.Round(time.Second)
|
||||
|
||||
results, err := globalSiteReplicationSys.Netperf(ctx, duration)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)), r.URL)
|
||||
return
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
if err := enc.Encode(results); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// NetperfHandler - perform mesh style network throughput test
|
||||
func (a adminAPIHandlers) NetperfHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "NetperfHandler")
|
||||
@@ -1525,7 +1573,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return shouldTrace(entry, traceOpts)
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1605,7 +1653,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2103,6 +2151,27 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
||||
}
|
||||
}
|
||||
|
||||
// collect all realtime metrics except disk
|
||||
// disk metrics are already included under drive info of each server
|
||||
getRealtimeMetrics := func() *madmin.RealtimeMetrics {
|
||||
var m madmin.RealtimeMetrics
|
||||
var types madmin.MetricType = madmin.MetricsAll &^ madmin.MetricsDisk
|
||||
mLocal := collectLocalMetrics(types, collectMetricsOpts{})
|
||||
m.Merge(&mLocal)
|
||||
cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
|
||||
mRemote := collectRemoteMetrics(cctx, types, collectMetricsOpts{})
|
||||
cancel()
|
||||
m.Merge(&mRemote)
|
||||
for idx, host := range m.Hosts {
|
||||
m.Hosts[idx] = anonAddr(host)
|
||||
}
|
||||
for host, metrics := range m.ByHost {
|
||||
m.ByHost[anonAddr(host)] = metrics
|
||||
delete(m.ByHost, host)
|
||||
}
|
||||
return &m
|
||||
}
|
||||
|
||||
anonymizeCmdLine := func(cmdLine string) string {
|
||||
if !globalIsDistErasure {
|
||||
// FS mode - single server - hard code to `server1`
|
||||
@@ -2280,6 +2349,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
||||
TLS: &tls,
|
||||
IsKubernetes: &isK8s,
|
||||
IsDocker: &isDocker,
|
||||
Metrics: getRealtimeMetrics(),
|
||||
}
|
||||
partialWrite(healthInfo)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, erro
|
||||
// Initialize boot time
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
globalEndpoints = mustGetPoolEndpoints(erasureDirs...)
|
||||
globalEndpoints = mustGetPoolEndpoints(0, erasureDirs...)
|
||||
|
||||
initAllSubsystems(ctx)
|
||||
|
||||
@@ -108,7 +108,7 @@ func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
endpoints := mustGetPoolEndpoints(erasureDirs...)
|
||||
endpoints := mustGetPoolEndpoints(0, erasureDirs...)
|
||||
globalPolicySys = NewPolicySys()
|
||||
objLayer, err := newErasureServerPools(ctx, endpoints)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
|
||||
+11
-9
@@ -22,15 +22,17 @@ import (
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
adminPathPrefix = minioReservedBucketPath + "/admin"
|
||||
adminAPIVersion = madmin.AdminAPIVersion
|
||||
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
|
||||
adminPathPrefix = minioReservedBucketPath + "/admin"
|
||||
adminAPIVersion = madmin.AdminAPIVersion
|
||||
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
|
||||
adminAPISiteReplicationDevNull = "/site-replication/devnull"
|
||||
adminAPISiteReplicationNetPerf = "/site-replication/netperf"
|
||||
)
|
||||
|
||||
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
||||
@@ -162,11 +164,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
HandlerFunc(gz(httpTraceHdrs(adminAPI.SetPolicyForUserOrGroup))).
|
||||
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
||||
|
||||
// Attach policies to user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/attach").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachPolicyBuiltin)))
|
||||
|
||||
// Detach policies from user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/detach").HandlerFunc(gz(httpTraceHdrs(adminAPI.DetachPolicyBuiltin)))
|
||||
// Attach/Detach policies to/from user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/{operation}").HandlerFunc(gz(httpTraceHdrs(adminAPI.AttachDetachPolicyBuiltin)))
|
||||
|
||||
// Remove user IAM
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveUser))).Queries("accessKey", "{accessKey:.*}")
|
||||
@@ -263,6 +262,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
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.MethodPost).Path(adminVersion + adminAPISiteReplicationDevNull).HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationDevNull)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPISiteReplicationNetPerf).HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationNetPerf)))
|
||||
|
||||
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:.*}")
|
||||
@@ -286,6 +287,7 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
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))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/site").HandlerFunc(httpTraceHdrs(adminAPI.SitePerfHandler))
|
||||
|
||||
// HTTP Trace
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(gz(http.HandlerFunc(adminAPI.TraceHandler)))
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
|
||||
+52
-28
@@ -31,7 +31,7 @@ import (
|
||||
"github.com/minio/minio/internal/ioutil"
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
@@ -87,6 +87,7 @@ const (
|
||||
ErrInternalError
|
||||
ErrInvalidAccessKeyID
|
||||
ErrAccessKeyDisabled
|
||||
ErrInvalidArgument
|
||||
ErrInvalidBucketName
|
||||
ErrInvalidDigest
|
||||
ErrInvalidRange
|
||||
@@ -149,6 +150,7 @@ const (
|
||||
ErrMethodNotAllowed
|
||||
ErrInvalidPart
|
||||
ErrInvalidPartOrder
|
||||
ErrMissingPart
|
||||
ErrAuthorizationHeaderMalformed
|
||||
ErrMalformedPOSTRequest
|
||||
ErrPOSTFileRequired
|
||||
@@ -182,8 +184,10 @@ const (
|
||||
ErrBucketAlreadyExists
|
||||
ErrMetadataTooLarge
|
||||
ErrUnsupportedMetadata
|
||||
ErrUnsupportedHostHeader
|
||||
ErrMaximumExpires
|
||||
ErrSlowDown
|
||||
ErrSlowDownRead
|
||||
ErrSlowDownWrite
|
||||
ErrInvalidPrefixMarker
|
||||
ErrBadRequest
|
||||
ErrKeyTooLongError
|
||||
@@ -255,9 +259,9 @@ const (
|
||||
ErrInvalidResourceName
|
||||
ErrInvalidLifecycleQueryParameter
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrRequestTimedout
|
||||
ErrClientDisconnected
|
||||
ErrOperationMaxedOut
|
||||
ErrTooManyRequests
|
||||
ErrInvalidRequest
|
||||
ErrTransitionStorageClassNotFoundError
|
||||
// MinIO storage class error codes
|
||||
@@ -561,6 +565,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Your account is disabled; please contact your administrator.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrInvalidArgument: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Invalid argument",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidBucketName: {
|
||||
Code: "InvalidBucketName",
|
||||
Description: "The specified bucket is not valid.",
|
||||
@@ -686,6 +695,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingPart: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "You must specify at least one part",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidPartOrder: {
|
||||
Code: "InvalidPartOrder",
|
||||
Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.",
|
||||
@@ -831,11 +845,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Request is not valid yet",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrSlowDown: {
|
||||
Code: "SlowDown",
|
||||
ErrSlowDownRead: {
|
||||
Code: "SlowDownRead",
|
||||
Description: "Resource requested is unreadable, please reduce your request rate",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrSlowDownWrite: {
|
||||
Code: "SlowDownWrite",
|
||||
Description: "Resource requested is unwritable, please reduce your request rate",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrInvalidPrefixMarker: {
|
||||
Code: "InvalidPrefixMarker",
|
||||
Description: "Invalid marker prefix combination",
|
||||
@@ -1109,8 +1128,13 @@ var errorCodes = errorCodeMap{
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionMethod: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The encryption method specified is not supported",
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server Side Encryption with AWS KMS managed key requires HTTP header x-amz-server-side-encryption : aws:kms",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIncompatibleEncryptionMethod: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server Side Encryption with Customer provided key is incompatible with the encryption method specified",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionKeyID: {
|
||||
@@ -1173,11 +1197,6 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The provided encryption parameters did not match the ones used originally.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrIncompatibleEncryptionMethod: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Server side encryption specified with both SSE-C and SSE-S3 headers",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrKMSNotConfigured: {
|
||||
Code: "NotImplemented",
|
||||
Description: "Server side encryption specified but KMS is not configured",
|
||||
@@ -1404,7 +1423,7 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrOperationTimedOut: {
|
||||
ErrRequestTimedout: {
|
||||
Code: "RequestTimeout",
|
||||
Description: "A timeout occurred while trying to lock a resource, please reduce your request rate",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
@@ -1414,9 +1433,9 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Client disconnected before response was ready",
|
||||
HTTPStatusCode: 499, // No official code, use nginx value.
|
||||
},
|
||||
ErrOperationMaxedOut: {
|
||||
Code: "SlowDown",
|
||||
Description: "A timeout exceeded while waiting to proceed with the request, please reduce your request rate",
|
||||
ErrTooManyRequests: {
|
||||
Code: "TooManyRequests",
|
||||
Description: "Deadline exceeded while waiting in incoming queue, please reduce your request rate",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrUnsupportedMetadata: {
|
||||
@@ -1424,6 +1443,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Your metadata headers are not supported.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrUnsupportedHostHeader: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your Host header is malformed.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectTampered: {
|
||||
Code: "XMinioObjectTampered",
|
||||
Description: errObjectTampered.Error(),
|
||||
@@ -2027,7 +2051,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
}
|
||||
|
||||
// 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 ErrRequestTimedout
|
||||
if contextCanceled(ctx) && errors.Is(ctx.Err(), context.Canceled) {
|
||||
return ErrClientDisconnected
|
||||
}
|
||||
@@ -2071,9 +2095,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case errInvalidStorageClass:
|
||||
apiErr = ErrInvalidStorageClass
|
||||
case errErasureReadQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
apiErr = ErrSlowDownRead
|
||||
case errErasureWriteQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
apiErr = ErrSlowDownWrite
|
||||
// SSE errors
|
||||
case errInvalidEncryptionParameters:
|
||||
apiErr = ErrInvalidEncryptionParameters
|
||||
@@ -2107,10 +2131,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrKMSKeyNotFoundException
|
||||
case errKMSDefaultKeyAlreadyConfigured:
|
||||
apiErr = ErrKMSDefaultKeyAlreadyConfigured
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
case errDiskNotFound:
|
||||
apiErr = ErrSlowDown
|
||||
case context.Canceled:
|
||||
apiErr = ErrClientDisconnected
|
||||
case context.DeadlineExceeded:
|
||||
apiErr = ErrRequestTimedout
|
||||
case objectlock.ErrInvalidRetentionDate:
|
||||
apiErr = ErrInvalidRetentionDate
|
||||
case objectlock.ErrPastObjectLockRetainDate:
|
||||
@@ -2189,9 +2213,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case InvalidPart:
|
||||
apiErr = ErrInvalidPart
|
||||
case InsufficientWriteQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
apiErr = ErrSlowDownWrite
|
||||
case InsufficientReadQuorum:
|
||||
apiErr = ErrSlowDown
|
||||
apiErr = ErrSlowDownRead
|
||||
case InvalidMarkerPrefixCombination:
|
||||
apiErr = ErrNotImplemented
|
||||
case InvalidUploadIDKeyCombination:
|
||||
@@ -2285,7 +2309,7 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case *event.ErrUnsupportedConfiguration:
|
||||
apiErr = ErrUnsupportedNotification
|
||||
case OperationTimedOut:
|
||||
apiErr = ErrOperationTimedOut
|
||||
apiErr = ErrRequestTimedout
|
||||
case BackendDown:
|
||||
apiErr = ErrBackendDown
|
||||
case ObjectNameTooLong:
|
||||
@@ -2380,7 +2404,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
}
|
||||
case lifecycle.Error:
|
||||
apiErr = APIError{
|
||||
Code: "InvalidRequest",
|
||||
Code: "InvalidArgument",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ var toAPIErrorTests = []struct {
|
||||
{err: ObjectNameInvalid{}, errCode: ErrInvalidObjectName},
|
||||
{err: InvalidUploadID{}, errCode: ErrNoSuchUpload},
|
||||
{err: InvalidPart{}, errCode: ErrInvalidPart},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrSlowDown},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDown},
|
||||
{err: InsufficientReadQuorum{}, errCode: ErrSlowDownRead},
|
||||
{err: InsufficientWriteQuorum{}, errCode: ErrSlowDownWrite},
|
||||
{err: InvalidMarkerPrefixCombination{}, errCode: ErrNotImplemented},
|
||||
{err: InvalidUploadIDKeyCombination{}, errCode: ErrNotImplemented},
|
||||
{err: MalformedUploadID{}, errCode: ErrNoSuchUpload},
|
||||
|
||||
+3
-3
@@ -153,7 +153,7 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
// Do not need to send any internal metadata
|
||||
// values to client.
|
||||
continue
|
||||
@@ -166,7 +166,7 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
|
||||
var isSet bool
|
||||
for _, userMetadataPrefix := range userMetadataKeyPrefixes {
|
||||
if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(userMetadataPrefix)) {
|
||||
if !stringsHasPrefixFold(k, userMetadataPrefix) {
|
||||
continue
|
||||
}
|
||||
w.Header()[strings.ToLower(k)] = []string{v}
|
||||
@@ -203,7 +203,7 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
}
|
||||
|
||||
// Set the relevant version ID as part of the response header.
|
||||
if objInfo.VersionID != "" {
|
||||
if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
|
||||
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -550,7 +550,7 @@ func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delim
|
||||
content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
|
||||
}
|
||||
for k, v := range cleanMinioInternalMetadataKeys(object.UserDefined) {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
// Do not need to send any internal metadata
|
||||
// values to client.
|
||||
continue
|
||||
@@ -693,7 +693,7 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
||||
content.UserMetadata.Set(xhttp.AmzServerSideEncryptionCustomerAlgorithm, xhttp.AmzEncryptionAES)
|
||||
}
|
||||
for k, v := range cleanMinioInternalMetadataKeys(object.UserDefined) {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
// Do not need to send any internal metadata
|
||||
// values to client.
|
||||
continue
|
||||
@@ -945,6 +945,8 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
||||
}
|
||||
|
||||
func writeErrorResponseHeadersOnly(w http.ResponseWriter, err APIError) {
|
||||
w.Header().Set(xMinIOErrCodeHeader, err.Code)
|
||||
w.Header().Set(xMinIOErrDescHeader, "\""+err.Description+"\"")
|
||||
writeResponse(w, err.HTTPStatusCode, nil, mimeNone)
|
||||
}
|
||||
|
||||
|
||||
+308
-304
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
@@ -30,9 +30,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/env"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -345,7 +347,9 @@ func initAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
|
||||
globalBackgroundHealState.pushHealLocalDisks(getLocalDisksToHeal()...)
|
||||
|
||||
go monitorLocalDisksAndHeal(ctx, z)
|
||||
if env.Get("_MINIO_AUTO_DISK_HEALING", config.EnableOn) == config.EnableOn {
|
||||
go monitorLocalDisksAndHeal(ctx, z)
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalDisksToHeal() (disksToHeal Endpoints) {
|
||||
|
||||
+11
-12
@@ -37,8 +37,7 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
miniogo "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
@@ -311,7 +310,7 @@ func (r BatchJobReplicateV1) Notify(ctx context.Context, body io.Reader) error {
|
||||
}
|
||||
|
||||
// ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
|
||||
func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
|
||||
func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
|
||||
srcBucket := r.Source.Bucket
|
||||
tgtBucket := r.Target.Bucket
|
||||
srcObject := srcObjInfo.Name
|
||||
@@ -360,7 +359,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
|
||||
}
|
||||
return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
|
||||
}
|
||||
gopts := minio.GetObjectOptions{
|
||||
gopts := miniogo.GetObjectOptions{
|
||||
VersionID: srcObjInfo.VersionID,
|
||||
}
|
||||
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
|
||||
@@ -381,7 +380,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
|
||||
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
|
||||
srcBucket := r.Source.Bucket
|
||||
tgtBucket := r.Target.Bucket
|
||||
srcObject := srcObjInfo.Name
|
||||
@@ -422,7 +421,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
|
||||
)
|
||||
|
||||
for i := 0; i < partsCount; i++ {
|
||||
gopts := minio.GetObjectOptions{
|
||||
gopts := miniogo.GetObjectOptions{
|
||||
VersionID: srcObjInfo.VersionID,
|
||||
PartNumber: i + 1,
|
||||
}
|
||||
@@ -521,7 +520,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
if len(r.Flags.Filter.Metadata) > 0 {
|
||||
for _, kv := range r.Flags.Filter.Metadata {
|
||||
for k, v := range oi.UserDefined {
|
||||
if !strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !isStandardHeader(k) {
|
||||
if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
|
||||
continue
|
||||
}
|
||||
// We only need to match x-amz-meta or standardHeaders
|
||||
@@ -556,7 +555,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
}
|
||||
|
||||
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
|
||||
core := &minio.Core{Client: c}
|
||||
core := &miniogo.Core{Client: c}
|
||||
|
||||
workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
|
||||
if err != nil {
|
||||
@@ -577,7 +576,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
s3Type := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
|
||||
minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
objInfoCh := c.ListObjects(ctx, r.Source.Bucket, minio.ListObjectsOptions{
|
||||
objInfoCh := c.ListObjects(ctx, r.Source.Bucket, miniogo.ListObjectsOptions{
|
||||
Prefix: r.Source.Prefix,
|
||||
WithVersions: minioSrc,
|
||||
Recursive: true,
|
||||
@@ -589,7 +588,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
for obj := range objInfoCh {
|
||||
oi := toObjectInfo(r.Source.Bucket, obj.Key, obj)
|
||||
if !minioSrc {
|
||||
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, minio.StatObjectOptions{})
|
||||
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{})
|
||||
if err == nil {
|
||||
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
|
||||
} else {
|
||||
@@ -671,7 +670,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
||||
}
|
||||
|
||||
// toObjectInfo converts minio.ObjectInfo to ObjectInfo
|
||||
func toObjectInfo(bucket, object string, objInfo minio.ObjectInfo) ObjectInfo {
|
||||
func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo {
|
||||
tags, _ := tags.MapToObjectTags(objInfo.UserTags)
|
||||
oi := ObjectInfo{
|
||||
Bucket: bucket,
|
||||
@@ -1076,7 +1075,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
||||
if len(r.Flags.Filter.Metadata) > 0 {
|
||||
for _, kv := range r.Flags.Filter.Metadata {
|
||||
for k, v := range info.Metadata {
|
||||
if !strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !isStandardHeader(k) {
|
||||
if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
|
||||
continue
|
||||
}
|
||||
// We only need to match x-amz-meta or standardHeaders
|
||||
|
||||
+2
-2
@@ -289,7 +289,7 @@ func (r *BatchJobKeyRotateV1) KeyRotate(ctx context.Context, api ObjectLayer, ob
|
||||
)
|
||||
encMetadata := make(map[string]string)
|
||||
for k, v := range oi.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
encMetadata[k] = v
|
||||
}
|
||||
}
|
||||
@@ -401,7 +401,7 @@ func (r *BatchJobKeyRotateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
||||
if len(r.Flags.Filter.Metadata) > 0 {
|
||||
for _, kv := range r.Flags.Filter.Metadata {
|
||||
for k, v := range info.Metadata {
|
||||
if !strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") && !isStandardHeader(k) {
|
||||
if !stringsHasPrefixFold(k, "x-amz-meta-") && !isStandardHeader(k) {
|
||||
continue
|
||||
}
|
||||
// We only need to match x-amz-meta or standardHeaders
|
||||
|
||||
@@ -165,9 +165,9 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
||||
b.rc, err = b.disk.ReadFileStream(context.TODO(), b.volume, b.filePath, streamOffset, b.tillOffset-streamOffset)
|
||||
if err != nil {
|
||||
if !IsErr(err, ignoredErrs...) {
|
||||
logger.LogIf(GlobalContext,
|
||||
logger.LogOnceIf(GlobalContext,
|
||||
fmt.Errorf("Reading erasure shards at (%s: %s/%s) returned '%w', will attempt to reconstruct if we have quorum",
|
||||
b.disk, b.volume, b.filePath, err))
|
||||
b.disk, b.volume, b.filePath, err), "bitrot-read-file-stream"+b.volume+b.filePath)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/pubsub"
|
||||
)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/kes-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
|
||||
+49
-11
@@ -20,7 +20,9 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
@@ -41,7 +43,7 @@ import (
|
||||
"github.com/minio/mux"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
@@ -654,6 +656,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
if deleteResult.errInfo.Code != "" {
|
||||
deleteErrors = append(deleteErrors, deleteResult.errInfo)
|
||||
} else {
|
||||
// All deletes on directory objects was with `nullVersionID`.
|
||||
// Remove it from response.
|
||||
if isDirObject(deleteResult.delInfo.ObjectName) && deleteResult.delInfo.VersionID == nullVersionID {
|
||||
deleteResult.delInfo.VersionID = ""
|
||||
}
|
||||
deletedObjects = append(deletedObjects, deleteResult.delInfo)
|
||||
}
|
||||
}
|
||||
@@ -669,6 +676,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
|
||||
// copy so we can re-add null ID.
|
||||
dobj := dobj
|
||||
if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
|
||||
dobj.VersionID = nullVersionID
|
||||
}
|
||||
dv := DeletedObjectReplicationInfo{
|
||||
DeletedObject: dobj,
|
||||
Bucket: bucket,
|
||||
@@ -1199,6 +1211,16 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(r.Header) && crypto.S3.IsRequested(r.Header) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(r.Header) && crypto.S3KMS.IsRequested(r.Header) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, crypto.ErrIncompatibleEncryptionMethod), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if crypto.SSEC.IsRequested(r.Header) && isReplicationEnabled(ctx, bucket) {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidEncryptionParametersSSEC), r.URL)
|
||||
return
|
||||
@@ -1270,15 +1292,20 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
buf := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(buf)
|
||||
|
||||
md5w := md5.New()
|
||||
|
||||
// Maximum allowed fan-out object size.
|
||||
const maxFanOutSize = 16 << 20
|
||||
|
||||
n, err := io.Copy(buf, ioutil.HardLimitReader(pReader, maxFanOutSize))
|
||||
n, err := io.Copy(io.MultiWriter(buf, md5w), ioutil.HardLimitReader(pReader, maxFanOutSize))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the correct hex md5sum for the fan-out stream.
|
||||
fanOutOpts.MD5Hex = hex.EncodeToString(md5w.Sum(nil))
|
||||
|
||||
concurrentSize := 100
|
||||
if runtime.GOMAXPROCS(0) < concurrentSize {
|
||||
concurrentSize = runtime.GOMAXPROCS(0)
|
||||
@@ -1303,7 +1330,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
if errs[i] != nil {
|
||||
fanOutResp = append(fanOutResp, minio.PutObjectFanOutResponse{
|
||||
Key: objInfo.Name,
|
||||
Error: errs[i],
|
||||
Error: errs[i].Error(),
|
||||
})
|
||||
|
||||
eventArgsList = append(eventArgsList, eventArgs{
|
||||
EventName: event.ObjectCreatedPost,
|
||||
BucketName: objInfo.Bucket,
|
||||
Object: ObjectInfo{Name: objInfo.Name},
|
||||
ReqParams: extractReqParams(r),
|
||||
RespElements: extractRespElements(w),
|
||||
UserAgent: fmt.Sprintf("%s MinIO-Fan-Out (failed: %v)", r.UserAgent(), errs[i]),
|
||||
Host: handlers.GetSourceIP(r),
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -1370,7 +1407,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
w.Header()[xhttp.ETag] = []string{`"` + objInfo.ETag + `"`}
|
||||
|
||||
// Set the relevant version ID as part of the response header.
|
||||
if objInfo.VersionID != "" {
|
||||
if objInfo.VersionID != "" && objInfo.VersionID != nullVersionID {
|
||||
w.Header()[xhttp.AmzVersionID] = []string{objInfo.VersionID}
|
||||
}
|
||||
|
||||
@@ -1512,10 +1549,7 @@ func (api objectAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ListBucketAction, bucket, ""); s3Error != ErrNone {
|
||||
errCode := errorCodes.ToAPIErr(s3Error)
|
||||
w.Header().Set(xMinIOErrCodeHeader, errCode.Code)
|
||||
w.Header().Set(xMinIOErrDescHeader, "\""+errCode.Description+"\"")
|
||||
writeErrorResponseHeadersOnly(w, errCode)
|
||||
writeErrorResponseHeadersOnly(w, errorCodes.ToAPIErr(s3Error))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1656,7 +1690,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
|
||||
config, err := objectlock.ParseObjectLockConfig(r.Body)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr := errorCodes.ToAPIErr(ErrInvalidArgument)
|
||||
apiErr.Description = err.Error()
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
@@ -1670,7 +1704,11 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
|
||||
// Deny object locking configuration settings on existing buckets without object lock enabled.
|
||||
if _, _, err = globalBucketMetadataSys.GetObjectLockConfig(bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
if _, ok := err.(BucketObjectLockConfigNotFound); ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL)
|
||||
} else {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1872,5 +1910,5 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
||||
}))
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
@@ -749,7 +749,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
for i := range requestList[0].Objects {
|
||||
var vid string
|
||||
if isDirObject(requestList[0].Objects[i].ObjectName) {
|
||||
vid = nullVersionID
|
||||
vid = ""
|
||||
}
|
||||
deletedObjects[i] = DeletedObject{
|
||||
ObjectName: requestList[0].Objects[i].ObjectName,
|
||||
@@ -766,7 +766,7 @@ func testAPIDeleteMultipleObjectsHandler(obj ObjectLayer, instanceType, bucketNa
|
||||
for i := range requestList[1].Objects {
|
||||
var vid string
|
||||
if isDirObject(requestList[0].Objects[i].ObjectName) {
|
||||
vid = nullVersionID
|
||||
vid = ""
|
||||
}
|
||||
deletedObjects[i] = DeletedObject{
|
||||
ObjectName: requestList[1].Objects[i].ObjectName,
|
||||
|
||||
@@ -179,7 +179,7 @@ func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName strin
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidRequest",
|
||||
Code: "InvalidArgument",
|
||||
Message: "Filter must have exactly one of Prefix, Tag, or And specified",
|
||||
},
|
||||
|
||||
@@ -196,7 +196,7 @@ func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName strin
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidRequest",
|
||||
Code: "InvalidArgument",
|
||||
Message: "Date must be provided in ISO 8601 format",
|
||||
},
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/amztime"
|
||||
sse "github.com/minio/minio/internal/bucket/encryption"
|
||||
@@ -691,7 +691,7 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
||||
|
||||
if rreq.Type == SelectRestoreRequest {
|
||||
for _, v := range rreq.OutputLocation.S3.UserMetadata {
|
||||
if !strings.HasPrefix(strings.ToLower(v.Name), "x-amz-meta") {
|
||||
if !stringsHasPrefixFold(v.Name, "x-amz-meta") {
|
||||
meta["x-amz-meta-"+v.Name] = v.Value
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
||||
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
bucketsse "github.com/minio/minio/internal/bucket/encryption"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
|
||||
@@ -83,12 +83,6 @@ func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locke
|
||||
// governance bypass headers are set and user has governance bypass permissions.
|
||||
// Objects in "Compliance" mode can be overwritten only if retention date is past.
|
||||
func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucket string, object ObjectToDelete, oi ObjectInfo, gerr error) APIErrorCode {
|
||||
opts, err := getOpts(ctx, r, bucket, object.ObjectName)
|
||||
if err != nil {
|
||||
return toAPIErrorCode(ctx, err)
|
||||
}
|
||||
|
||||
opts.VersionID = object.VersionID
|
||||
if gerr != nil { // error from GetObjectInfo
|
||||
if _, ok := gerr.(MethodNotAllowed); ok {
|
||||
// This happens usually for a delete marker
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
@@ -512,7 +512,10 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
||||
ObjectName: oi.Name,
|
||||
VersionID: oi.VersionID,
|
||||
},
|
||||
}, oi, ObjectOptions{VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name)}, nil)
|
||||
}, oi, ObjectOptions{
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
||||
}, nil)
|
||||
} else {
|
||||
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
|
||||
UserDefined: oi.UserDefined,
|
||||
|
||||
+129
-84
@@ -35,7 +35,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/minio/minio/internal/hash"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/workers"
|
||||
"github.com/zeebo/xxh3"
|
||||
)
|
||||
|
||||
@@ -169,7 +170,7 @@ func checkRemoteEndpoint(ctx context.Context, epURL *url.URL) error {
|
||||
reqURL := &url.URL{
|
||||
Scheme: epURL.Scheme,
|
||||
Host: epURL.Host,
|
||||
Path: healthCheckReadinessPath,
|
||||
Path: healthCheckPathPrefix + healthCheckReadinessPath,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
|
||||
@@ -604,7 +605,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
||||
return
|
||||
}
|
||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", dobj.Bucket, tgt.ARN))
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", dobj.Bucket, tgt.ARN), "replication-target-offline-delete"+tgt.ARN)
|
||||
sendEvent(eventArgs{
|
||||
BucketName: dobj.Bucket,
|
||||
Object: ObjectInfo{
|
||||
@@ -678,7 +679,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
||||
func getCopyObjMetadata(oi ObjectInfo, sc string) map[string]string {
|
||||
meta := make(map[string]string, len(oi.UserDefined))
|
||||
for k, v := range oi.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -743,7 +744,7 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, err error) {
|
||||
meta := make(map[string]string)
|
||||
for k, v := range objInfo.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
continue
|
||||
}
|
||||
if isStandardHeader(k) {
|
||||
@@ -908,7 +909,7 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
for k, v := range oi1.UserDefined {
|
||||
var found bool
|
||||
for _, prefix := range compareKeys {
|
||||
if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(prefix)) {
|
||||
if !stringsHasPrefixFold(k, prefix) {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
@@ -923,7 +924,7 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
||||
for k, v := range oi2.Metadata {
|
||||
var found bool
|
||||
for _, prefix := range compareKeys {
|
||||
if !strings.HasPrefix(strings.ToLower(k), strings.ToLower(prefix)) {
|
||||
if !stringsHasPrefixFold(k, prefix) {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
@@ -969,7 +970,7 @@ func replicateObject(ctx context.Context, ri ReplicateObjectInfo, objectAPI Obje
|
||||
|
||||
cfg, err := getReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "get-replication-config"+bucket)
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
@@ -1120,7 +1121,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
||||
}
|
||||
|
||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN))
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN), "replication-target-offline-obj"+tgt.ARN)
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
@@ -1270,7 +1271,7 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
}
|
||||
|
||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||
logger.LogIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN))
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("remote target is offline for bucket:%s arn:%s", bucket, tgt.ARN), "replication-target-offline-all"+tgt.ARN)
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
@@ -1383,6 +1384,25 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
||||
return
|
||||
}
|
||||
}
|
||||
// if target returns error other than NoSuchKey, defer replication attempt
|
||||
if cerr != nil {
|
||||
errResp := minio.ToErrorResponse(cerr)
|
||||
switch errResp.Code {
|
||||
case "NoSuchKey", "NoSuchVersion", "SlowDownRead":
|
||||
rAction = replicateAll
|
||||
default:
|
||||
logger.LogIf(ctx, fmt.Errorf("unable to replicate %s/%s (%s). Target (%s) returned %s error on HEAD",
|
||||
bucket, object, objInfo.VersionID, tgt.EndpointURL(), cerr))
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectReplicationNotTracked,
|
||||
BucketName: bucket,
|
||||
Object: objInfo,
|
||||
UserAgent: "Internal: [Replication]",
|
||||
Host: globalLocalNodeName,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
rinfo.ReplicationStatus = replication.Completed
|
||||
rinfo.Size = size
|
||||
rinfo.ReplicationAction = rAction
|
||||
@@ -2093,6 +2113,7 @@ func proxyHeadToRepTarget(ctx context.Context, bucket, object string, rs *HTTPRa
|
||||
StorageClass: objInfo.StorageClass,
|
||||
ReplicationStatusInternal: objInfo.ReplicationStatus,
|
||||
UserTags: tags.String(),
|
||||
ReplicationStatus: replication.StatusType(objInfo.ReplicationStatus),
|
||||
}
|
||||
oi.UserDefined = make(map[string]string, len(objInfo.Metadata))
|
||||
for k, v := range objInfo.Metadata {
|
||||
@@ -2307,7 +2328,10 @@ func (s *replicationResyncer) PersistToDisk(ctx context.Context, objectAPI Objec
|
||||
}
|
||||
}
|
||||
|
||||
const resyncWorkerCnt = 50 // limit of number of bucket resyncs is progress at any given time
|
||||
const (
|
||||
resyncWorkerCnt = 10 // limit of number of bucket resyncs is progress at any given time
|
||||
resyncParallelRoutines = 10 // number of parallel resync ops per bucket
|
||||
)
|
||||
|
||||
func newresyncer() *replicationResyncer {
|
||||
rs := replicationResyncer{
|
||||
@@ -2322,6 +2346,36 @@ func newresyncer() *replicationResyncer {
|
||||
return &rs
|
||||
}
|
||||
|
||||
// mark status of replication resync on remote target for the bucket
|
||||
func (s *replicationResyncer) markStatus(status ResyncStatusType, opts resyncOpts) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
m := s.statusMap[opts.bucket]
|
||||
st := m.TargetsMap[opts.arn]
|
||||
st.LastUpdate = UTCNow()
|
||||
st.ResyncStatus = status
|
||||
m.TargetsMap[opts.arn] = st
|
||||
m.LastUpdate = UTCNow()
|
||||
s.statusMap[opts.bucket] = m
|
||||
}
|
||||
|
||||
// update replication resync stats for bucket's remote target
|
||||
func (s *replicationResyncer) incStats(ts TargetReplicationResyncStatus, opts resyncOpts) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
m := s.statusMap[opts.bucket]
|
||||
st := m.TargetsMap[opts.arn]
|
||||
st.Object = ts.Object
|
||||
st.ReplicatedCount += ts.ReplicatedCount
|
||||
st.FailedCount += ts.FailedCount
|
||||
st.ReplicatedSize += ts.ReplicatedSize
|
||||
st.FailedSize += ts.FailedSize
|
||||
m.TargetsMap[opts.arn] = st
|
||||
m.LastUpdate = UTCNow()
|
||||
s.statusMap[opts.bucket] = m
|
||||
}
|
||||
|
||||
// resyncBucket resyncs all qualifying objects as per replication rules for the target
|
||||
// ARN
|
||||
func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI ObjectLayer, heal bool, opts resyncOpts) {
|
||||
@@ -2333,15 +2387,7 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
|
||||
|
||||
resyncStatus := ResyncFailed
|
||||
defer func() {
|
||||
s.Lock()
|
||||
m := s.statusMap[opts.bucket]
|
||||
st := m.TargetsMap[opts.arn]
|
||||
st.LastUpdate = UTCNow()
|
||||
st.ResyncStatus = resyncStatus
|
||||
m.TargetsMap[opts.arn] = st
|
||||
m.LastUpdate = UTCNow()
|
||||
s.statusMap[opts.bucket] = m
|
||||
s.Unlock()
|
||||
s.markStatus(resyncStatus, opts)
|
||||
globalSiteResyncMetrics.incBucket(opts, resyncStatus)
|
||||
s.workerCh <- struct{}{}
|
||||
}()
|
||||
@@ -2377,15 +2423,9 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
|
||||
}
|
||||
// mark resync status as resync started
|
||||
if !heal {
|
||||
s.Lock()
|
||||
m := s.statusMap[opts.bucket]
|
||||
st := m.TargetsMap[opts.arn]
|
||||
st.ResyncStatus = ResyncStarted
|
||||
m.TargetsMap[opts.arn] = st
|
||||
m.LastUpdate = UTCNow()
|
||||
s.statusMap[opts.bucket] = m
|
||||
s.Unlock()
|
||||
s.markStatus(ResyncStarted, opts)
|
||||
}
|
||||
|
||||
// Walk through all object versions - Walk() is always in ascending order needed to ensure
|
||||
// delete marker replicated to target after object version is first created.
|
||||
if err := objectAPI.Walk(ctx, opts.bucket, "", objInfoCh, ObjectOptions{}); err != nil {
|
||||
@@ -2401,80 +2441,85 @@ func (s *replicationResyncer) resyncBucket(ctx context.Context, objectAPI Object
|
||||
if st.ResyncStatus == ResyncStarted || st.ResyncStatus == ResyncFailed {
|
||||
lastCheckpoint = st.Object
|
||||
}
|
||||
workers, err := workers.New(resyncParallelRoutines)
|
||||
for obj := range objInfoCh {
|
||||
select {
|
||||
case <-s.resyncCancelCh:
|
||||
resyncStatus = ResyncCanceled
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
if heal && lastCheckpoint != "" && lastCheckpoint != obj.Name {
|
||||
continue
|
||||
}
|
||||
lastCheckpoint = ""
|
||||
|
||||
roi := getHealReplicateObjectInfo(obj, rcfg)
|
||||
if !roi.ExistingObjResync.mustResync() {
|
||||
continue
|
||||
}
|
||||
traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID))
|
||||
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
|
||||
versionID := ""
|
||||
dmVersionID := ""
|
||||
if roi.VersionPurgeStatus.Empty() {
|
||||
dmVersionID = roi.VersionID
|
||||
} else {
|
||||
versionID = roi.VersionID
|
||||
obj := obj
|
||||
workers.Take()
|
||||
go func() {
|
||||
defer workers.Give()
|
||||
roi := getHealReplicateObjectInfo(obj, rcfg)
|
||||
if !roi.ExistingObjResync.mustResync() {
|
||||
return
|
||||
}
|
||||
traceFn := s.trace(tgt.ResetID, fmt.Sprintf("%s/%s (%s)", opts.bucket, roi.Name, roi.VersionID))
|
||||
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
|
||||
versionID := ""
|
||||
dmVersionID := ""
|
||||
if roi.VersionPurgeStatus.Empty() {
|
||||
dmVersionID = roi.VersionID
|
||||
} else {
|
||||
versionID = roi.VersionID
|
||||
}
|
||||
|
||||
doi := DeletedObjectReplicationInfo{
|
||||
DeletedObject: DeletedObject{
|
||||
ObjectName: roi.Name,
|
||||
DeleteMarkerVersionID: dmVersionID,
|
||||
VersionID: versionID,
|
||||
ReplicationState: roi.getReplicationState(roi.Dsc.String(), versionID, true),
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
|
||||
DeleteMarker: roi.DeleteMarker,
|
||||
doi := DeletedObjectReplicationInfo{
|
||||
DeletedObject: DeletedObject{
|
||||
ObjectName: roi.Name,
|
||||
DeleteMarkerVersionID: dmVersionID,
|
||||
VersionID: versionID,
|
||||
ReplicationState: roi.getReplicationState(roi.Dsc.String(), versionID, true),
|
||||
DeleteMarkerMTime: DeleteMarkerMTime{roi.ModTime},
|
||||
DeleteMarker: roi.DeleteMarker,
|
||||
},
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.ExistingObjectReplicationType,
|
||||
EventType: ReplicateExistingDelete,
|
||||
}
|
||||
replicateDelete(ctx, doi, objectAPI)
|
||||
} else {
|
||||
roi.OpType = replication.ExistingObjectReplicationType
|
||||
roi.EventType = ReplicateExisting
|
||||
replicateObject(ctx, roi, objectAPI)
|
||||
}
|
||||
_, err = tgt.StatObject(ctx, tgt.Bucket, roi.Name, minio.StatObjectOptions{
|
||||
VersionID: roi.VersionID,
|
||||
Internal: minio.AdvancedGetOptions{
|
||||
ReplicationProxyRequest: "false",
|
||||
},
|
||||
Bucket: roi.Bucket,
|
||||
OpType: replication.ExistingObjectReplicationType,
|
||||
EventType: ReplicateExistingDelete,
|
||||
})
|
||||
st := TargetReplicationResyncStatus{
|
||||
Object: roi.Name,
|
||||
Bucket: roi.Bucket,
|
||||
}
|
||||
replicateDelete(ctx, doi, objectAPI)
|
||||
} else {
|
||||
roi.OpType = replication.ExistingObjectReplicationType
|
||||
roi.EventType = ReplicateExisting
|
||||
replicateObject(ctx, roi, objectAPI)
|
||||
}
|
||||
_, err = tgt.StatObject(ctx, tgt.Bucket, roi.Name, minio.StatObjectOptions{
|
||||
VersionID: roi.VersionID,
|
||||
Internal: minio.AdvancedGetOptions{
|
||||
ReplicationProxyRequest: "false",
|
||||
},
|
||||
})
|
||||
s.Lock()
|
||||
m = s.statusMap[opts.bucket]
|
||||
st = m.TargetsMap[opts.arn]
|
||||
st.Object = roi.Name
|
||||
success := true
|
||||
if err != nil {
|
||||
if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
|
||||
st.ReplicatedCount++
|
||||
success := true
|
||||
if err != nil {
|
||||
if roi.DeleteMarker && isErrMethodNotAllowed(ErrorRespToObjectError(err, opts.bucket, roi.Name)) {
|
||||
st.ReplicatedCount++
|
||||
} else {
|
||||
st.FailedCount++
|
||||
success = false
|
||||
}
|
||||
} else {
|
||||
st.FailedCount++
|
||||
success = false
|
||||
st.ReplicatedCount++
|
||||
st.ReplicatedSize += roi.Size
|
||||
}
|
||||
} else {
|
||||
st.ReplicatedCount++
|
||||
st.ReplicatedSize += roi.Size
|
||||
}
|
||||
m.TargetsMap[opts.arn] = st
|
||||
m.LastUpdate = UTCNow()
|
||||
s.statusMap[opts.bucket] = m
|
||||
s.Unlock()
|
||||
traceFn(err)
|
||||
globalSiteResyncMetrics.updateMetric(roi, success, opts.resyncID)
|
||||
s.incStats(st, opts)
|
||||
traceFn(err)
|
||||
globalSiteResyncMetrics.updateMetric(roi, success, opts.resyncID)
|
||||
}()
|
||||
}
|
||||
workers.Wait()
|
||||
resyncStatus = ResyncCompleted
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
|
||||
+6
-20
@@ -52,7 +52,7 @@ import (
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/console/restapi/operations"
|
||||
"github.com/minio/kes-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
@@ -66,7 +66,6 @@ import (
|
||||
"github.com/minio/pkg/ellipses"
|
||||
"github.com/minio/pkg/env"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
"github.com/rs/dnscache"
|
||||
)
|
||||
|
||||
// serverDebugLog will enable debug printing
|
||||
@@ -96,11 +95,6 @@ func init() {
|
||||
|
||||
initGlobalContext()
|
||||
|
||||
options := dnscache.ResolverRefreshOptions{
|
||||
ClearUnused: true,
|
||||
PersistOnFailure: false,
|
||||
}
|
||||
|
||||
t, _ := minioVersionToReleaseTime(Version)
|
||||
if !t.IsZero() {
|
||||
globalVersionUnix = uint64(t.Unix())
|
||||
@@ -108,24 +102,16 @@ func init() {
|
||||
|
||||
globalIsCICD = env.Get("MINIO_CI_CD", "") != "" || env.Get("CI", "") != ""
|
||||
|
||||
containers := IsKubernetes() || IsDocker() || IsBOSH() || IsDCOS() || IsPCFTile()
|
||||
|
||||
// Call to refresh will refresh names in cache. If you pass true, it will also
|
||||
// remove cached names not looked up since the last call to Refresh. It is a good idea
|
||||
// to call this method on a regular interval.
|
||||
// Call to refresh will refresh names in cache.
|
||||
go func() {
|
||||
var t *time.Ticker
|
||||
if containers {
|
||||
// k8s DNS TTL is 30s (Attempt a refresh only after)
|
||||
t = time.NewTicker(30 * time.Second)
|
||||
} else {
|
||||
t = time.NewTicker(10 * time.Minute)
|
||||
}
|
||||
// Baremetal setups set DNS refresh window to 10 minutes.
|
||||
t := time.NewTicker(10 * time.Minute)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
globalDNSCache.RefreshWithOptions(options)
|
||||
globalDNSCache.Refresh()
|
||||
|
||||
case <-GlobalContext.Done():
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/api"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
@@ -616,6 +616,9 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
}
|
||||
for n, l := range loggerCfg.AuditKafka {
|
||||
if l.Enabled {
|
||||
if l.TLS.Enable {
|
||||
l.TLS.RootCAs = globalRootCAs
|
||||
}
|
||||
l.LogOnce = logger.LogOnceIf
|
||||
loggerCfg.AuditKafka[n] = l
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import (
|
||||
"strings"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/logger/target/console"
|
||||
"github.com/minio/minio/internal/logger/target/types"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
)
|
||||
|
||||
|
||||
+7
-4
@@ -34,7 +34,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
@@ -373,7 +373,7 @@ func (f *folderScanner) sendUpdate() {
|
||||
}
|
||||
if flat := f.updateCache.sizeRecursive(f.newCache.Info.Name); flat != nil {
|
||||
select {
|
||||
case f.updates <- *flat:
|
||||
case f.updates <- flat.clone():
|
||||
default:
|
||||
}
|
||||
f.lastUpdate = time.Now()
|
||||
@@ -941,7 +941,7 @@ func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi Obje
|
||||
console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action)
|
||||
}
|
||||
}
|
||||
defer globalScannerMetrics.timeILM(lcEvt.Action)
|
||||
defer globalScannerMetrics.timeILM(lcEvt.Action)()
|
||||
|
||||
switch lcEvt.Action {
|
||||
case lifecycle.DeleteAction, lifecycle.DeleteVersionAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||
@@ -1187,6 +1187,9 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
||||
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
|
||||
opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
|
||||
}
|
||||
if lcEvent.Action.DeleteAll() {
|
||||
opts.DeletePrefix = true
|
||||
}
|
||||
|
||||
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
||||
if err != nil {
|
||||
@@ -1194,7 +1197,7 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
||||
return false
|
||||
}
|
||||
// Assume it is still there.
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "non-transition-expiry")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+37
-7
@@ -33,7 +33,7 @@ import (
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -88,6 +88,20 @@ func (ats *allTierStats) merge(other *allTierStats) {
|
||||
}
|
||||
}
|
||||
|
||||
func (ats *allTierStats) clone() *allTierStats {
|
||||
if ats == nil {
|
||||
return nil
|
||||
}
|
||||
dst := *ats
|
||||
if dst.Tiers != nil {
|
||||
dst.Tiers = make(map[string]tierStats, len(dst.Tiers))
|
||||
for tier, st := range dst.Tiers {
|
||||
dst.Tiers[tier] = st
|
||||
}
|
||||
}
|
||||
return &dst
|
||||
}
|
||||
|
||||
func (ats *allTierStats) adminStats(stats map[string]madmin.TierStats) map[string]madmin.TierStats {
|
||||
if ats == nil {
|
||||
return stats
|
||||
@@ -168,6 +182,25 @@ type replicationAllStatsV1 struct {
|
||||
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
|
||||
}
|
||||
|
||||
// clone creates a deep-copy clone.
|
||||
func (r *replicationAllStats) clone() *replicationAllStats {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shallow copy
|
||||
dst := *r
|
||||
|
||||
// Copy individual targets.
|
||||
if dst.Targets != nil {
|
||||
dst.Targets = make(map[string]replicationStats, len(dst.Targets))
|
||||
for k, v := range r.Targets {
|
||||
dst.Targets[k] = v
|
||||
}
|
||||
}
|
||||
return &dst
|
||||
}
|
||||
|
||||
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
|
||||
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6
|
||||
|
||||
@@ -413,14 +446,11 @@ func (e dataUsageEntry) clone() dataUsageEntry {
|
||||
e.Children = ch
|
||||
}
|
||||
if e.ReplicationStats != nil {
|
||||
// Copy to new struct
|
||||
r := *e.ReplicationStats
|
||||
e.ReplicationStats = &r
|
||||
// Clone ReplicationStats
|
||||
e.ReplicationStats = e.ReplicationStats.clone()
|
||||
}
|
||||
if e.AllTierStats != nil {
|
||||
ats := newAllTierStats()
|
||||
ats.merge(e.AllTierStats)
|
||||
e.AllTierStats = ats
|
||||
e.AllTierStats = e.AllTierStats.clone()
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
// BucketTargetUsageInfo - bucket target usage info provides
|
||||
|
||||
+2
-2
@@ -139,14 +139,14 @@ func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *disk
|
||||
bkMeta := make(map[string]string, len(bkObjectInfo.UserDefined))
|
||||
cacheMeta := make(map[string]string, len(cacheObjInfo.UserDefined))
|
||||
for k, v := range bkObjectInfo.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
bkMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
for k, v := range cacheObjInfo.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -359,6 +359,9 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
for i := range endpointList {
|
||||
endpointList[i].SetPool(0)
|
||||
}
|
||||
endpointServerPools = append(endpointServerPools, PoolEndpoints{
|
||||
Legacy: true,
|
||||
SetCount: len(setArgs),
|
||||
|
||||
+52
-3
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/minio/minio/internal/mountinfo"
|
||||
"github.com/minio/pkg/env"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// EndpointType - enum for endpoint type.
|
||||
@@ -58,9 +59,17 @@ type ProxyEndpoint struct {
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// Node holds information about a node in this cluster
|
||||
type Node struct {
|
||||
*url.URL
|
||||
Pools []int
|
||||
IsLocal bool
|
||||
}
|
||||
|
||||
// Endpoint - any type of endpoint.
|
||||
type Endpoint struct {
|
||||
*url.URL
|
||||
Pool int
|
||||
IsLocal bool
|
||||
}
|
||||
|
||||
@@ -97,6 +106,11 @@ func (endpoint *Endpoint) UpdateIsLocal() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPool sets a specific pool number to this node
|
||||
func (endpoint *Endpoint) SetPool(i int) {
|
||||
endpoint.Pool = i
|
||||
}
|
||||
|
||||
// NewEndpoint - returns new endpoint based on given arguments.
|
||||
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
// isEmptyPath - check whether given path is not empty.
|
||||
@@ -209,6 +223,35 @@ type PoolEndpoints struct {
|
||||
// EndpointServerPools - list of list of endpoints
|
||||
type EndpointServerPools []PoolEndpoints
|
||||
|
||||
// GetNodes returns a sorted list of nodes in this cluster
|
||||
func (l EndpointServerPools) GetNodes() (nodes []Node) {
|
||||
nodesMap := make(map[string]Node)
|
||||
for _, pool := range l {
|
||||
for _, ep := range pool.Endpoints {
|
||||
node, ok := nodesMap[ep.Host]
|
||||
if !ok {
|
||||
node.IsLocal = ep.IsLocal
|
||||
node.URL = &url.URL{
|
||||
Scheme: ep.Scheme,
|
||||
Host: ep.Host,
|
||||
}
|
||||
}
|
||||
if !slices.Contains(node.Pools, ep.Pool) {
|
||||
node.Pools = append(node.Pools, ep.Pool)
|
||||
}
|
||||
nodesMap[ep.Host] = node
|
||||
}
|
||||
}
|
||||
nodes = make([]Node, 0, len(nodesMap))
|
||||
for _, v := range nodesMap {
|
||||
nodes = append(nodes, v)
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].Host < nodes[j].Host
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// GetPoolIdx return pool index
|
||||
func (l EndpointServerPools) GetPoolIdx(pool string) int {
|
||||
for id, ep := range globalEndpoints {
|
||||
@@ -768,6 +811,8 @@ func CreatePoolEndpoints(serverAddr string, poolArgs ...[][]string) ([]Endpoints
|
||||
return nil, setupType, config.ErrInvalidEndpoint(nil).Msg("use path style endpoint for single node setup")
|
||||
}
|
||||
|
||||
endpoint.SetPool(0)
|
||||
|
||||
var endpoints Endpoints
|
||||
endpoints = append(endpoints, endpoint)
|
||||
setupType = ErasureSDSetupType
|
||||
@@ -781,7 +826,7 @@ func CreatePoolEndpoints(serverAddr string, poolArgs ...[][]string) ([]Endpoints
|
||||
return poolEndpoints, setupType, nil
|
||||
}
|
||||
|
||||
for i, args := range poolArgs {
|
||||
for poolIdx, args := range poolArgs {
|
||||
var endpoints Endpoints
|
||||
for _, iargs := range args {
|
||||
// Convert args to endpoints
|
||||
@@ -795,6 +840,10 @@ func CreatePoolEndpoints(serverAddr string, poolArgs ...[][]string) ([]Endpoints
|
||||
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
|
||||
}
|
||||
|
||||
for i := range eps {
|
||||
eps[i].SetPool(poolIdx)
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, eps...)
|
||||
}
|
||||
|
||||
@@ -802,7 +851,7 @@ func CreatePoolEndpoints(serverAddr string, poolArgs ...[][]string) ([]Endpoints
|
||||
return nil, setupType, config.ErrInvalidErasureEndpoints(nil).Msg("invalid number of endpoints")
|
||||
}
|
||||
|
||||
poolEndpoints[i] = endpoints
|
||||
poolEndpoints[poolIdx] = endpoints
|
||||
}
|
||||
|
||||
for _, endpoints := range poolEndpoints {
|
||||
@@ -960,7 +1009,7 @@ func CreateEndpoints(serverAddr string, args ...[]string) (Endpoints, SetupType,
|
||||
|
||||
_, serverAddrPort := mustSplitHostPort(serverAddr)
|
||||
|
||||
// For single arg, return FS setup.
|
||||
// For single arg, return single drive setup.
|
||||
if len(args) == 1 && len(args[0]) == 1 {
|
||||
var endpoint Endpoint
|
||||
endpoint, err = NewEndpoint(args[0][0])
|
||||
|
||||
@@ -372,7 +372,7 @@ func TestGetLocalPeer(t *testing.T) {
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...)
|
||||
zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...)
|
||||
if !zendpoints[0].Endpoints[0].IsLocal {
|
||||
if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
|
||||
t.Fatalf("error: expected = <nil>, got = %v", err)
|
||||
@@ -405,7 +405,7 @@ func TestGetRemotePeers(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
zendpoints := mustGetPoolEndpoints(testCase.endpointArgs...)
|
||||
zendpoints := mustGetPoolEndpoints(0, testCase.endpointArgs...)
|
||||
if !zendpoints[0].Endpoints[0].IsLocal {
|
||||
if err := zendpoints[0].Endpoints.UpdateIsLocal(); err != nil {
|
||||
t.Errorf("error: expected = <nil>, got = %v", err)
|
||||
|
||||
@@ -317,7 +317,7 @@ func (e Erasure) Heal(ctx context.Context, writers []io.Writer, readers []io.Rea
|
||||
}
|
||||
|
||||
if err = e.DecodeDataAndParityBlocks(ctx, bufs); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-heal-decode")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ func (e Erasure) Heal(ctx context.Context, writers []io.Writer, readers []io.Rea
|
||||
}
|
||||
|
||||
if err = w.Write(ctx, bufs); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-heal-write")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,41 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func commonETags(etags []string) (etag string, maxima int) {
|
||||
etagOccurrenceMap := make(map[string]int, len(etags))
|
||||
|
||||
// Ignore the uuid sentinel and count the rest.
|
||||
for _, etag := range etags {
|
||||
if etag == "" {
|
||||
continue
|
||||
}
|
||||
etagOccurrenceMap[etag]++
|
||||
}
|
||||
|
||||
maxima = 0 // Counter for remembering max occurrence of elements.
|
||||
latest := ""
|
||||
|
||||
// Find the common cardinality from previously collected
|
||||
// occurrences of elements.
|
||||
for etag, count := range etagOccurrenceMap {
|
||||
if count < maxima {
|
||||
continue
|
||||
}
|
||||
|
||||
// We are at or above maxima
|
||||
if count > maxima {
|
||||
maxima = count
|
||||
latest = etag
|
||||
}
|
||||
}
|
||||
|
||||
// Return the collected common max time, with maxima
|
||||
return latest, maxima
|
||||
}
|
||||
|
||||
// commonTime returns a maximally occurring time from a list of time.
|
||||
func commonTimeAndOccurence(times []time.Time, group time.Duration) (maxTime time.Time, maxima int) {
|
||||
timeOccurenceMap := make(map[int64]int, len(times))
|
||||
@@ -86,6 +118,13 @@ func commonTime(modTimes []time.Time, quorum int) time.Time {
|
||||
return timeSentinel
|
||||
}
|
||||
|
||||
func commonETag(etags []string, quorum int) string {
|
||||
if etag, count := commonETags(etags); count >= quorum {
|
||||
return etag
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Beginning of unix time is treated as sentinel value here.
|
||||
var (
|
||||
timeSentinel = time.Unix(0, 0).UTC()
|
||||
@@ -102,6 +141,33 @@ func bootModtimes(diskCount int) []time.Time {
|
||||
return modTimes
|
||||
}
|
||||
|
||||
func listObjectETags(partsMetadata []FileInfo, errs []error, quorum int) (etags []string) {
|
||||
etags = make([]string, len(partsMetadata))
|
||||
vidMap := map[string]int{}
|
||||
for index, metadata := range partsMetadata {
|
||||
if errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
vid := metadata.VersionID
|
||||
if metadata.VersionID == "" {
|
||||
vid = nullVersionID
|
||||
}
|
||||
vidMap[vid]++
|
||||
etags[index] = metadata.Metadata["etag"]
|
||||
}
|
||||
|
||||
for _, count := range vidMap {
|
||||
// do we have enough common versions
|
||||
// that have enough quorum to satisfy
|
||||
// the etag.
|
||||
if count >= quorum {
|
||||
return etags
|
||||
}
|
||||
}
|
||||
|
||||
return make([]string, len(partsMetadata))
|
||||
}
|
||||
|
||||
// Extracts list of times from FileInfo slice and returns, skips
|
||||
// slice elements which have errors.
|
||||
func listObjectModtimes(partsMetadata []FileInfo, errs []error) (modTimes []time.Time) {
|
||||
@@ -162,7 +228,7 @@ func listObjectDiskMtimes(partsMetadata []FileInfo) (diskMTimes []time.Time) {
|
||||
// - a slice of disks where disk having 'older' xl.meta (or nothing)
|
||||
// are set to nil.
|
||||
// - latest (in time) of the maximally occurring modTime(s), which has at least quorum occurrences.
|
||||
func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error, quorum int) (onlineDisks []StorageAPI, modTime time.Time) {
|
||||
func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error, quorum int) (onlineDisks []StorageAPI, modTime time.Time, etag string) {
|
||||
onlineDisks = make([]StorageAPI, len(disks))
|
||||
|
||||
// List all the file commit ids from parts metadata.
|
||||
@@ -171,6 +237,23 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
|
||||
// Reduce list of UUIDs to a single common value.
|
||||
modTime = commonTime(modTimes, quorum)
|
||||
|
||||
if modTime.IsZero() || modTime.Equal(timeSentinel) {
|
||||
etags := listObjectETags(partsMetadata, errs, quorum)
|
||||
|
||||
etag = commonETag(etags, quorum)
|
||||
|
||||
if etag != "" { // allow this fallback only if a non-empty etag is found.
|
||||
for index, e := range etags {
|
||||
if partsMetadata[index].IsValid() && e == etag {
|
||||
onlineDisks[index] = disks[index]
|
||||
} else {
|
||||
onlineDisks[index] = nil
|
||||
}
|
||||
}
|
||||
return onlineDisks, modTime, etag
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new online disks slice, which have common uuid.
|
||||
for index, t := range modTimes {
|
||||
if partsMetadata[index].IsValid() && t.Equal(modTime) {
|
||||
@@ -180,7 +263,7 @@ func listOnlineDisks(disks []StorageAPI, partsMetadata []FileInfo, errs []error,
|
||||
}
|
||||
}
|
||||
|
||||
return onlineDisks, modTime
|
||||
return onlineDisks, modTime, ""
|
||||
}
|
||||
|
||||
// disksWithAllParts - This function needs to be called with
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
// Returns the latest updated FileInfo files and error in case of failure.
|
||||
@@ -299,7 +299,7 @@ func TestListOnlineDisks(t *testing.T) {
|
||||
}
|
||||
|
||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||
onlineDisks, modTime := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
|
||||
onlineDisks, modTime, _ := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
|
||||
if !modTime.Equal(test.expectedTime) {
|
||||
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
||||
test.expectedTime, modTime)
|
||||
@@ -481,7 +481,7 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
||||
}
|
||||
|
||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||
onlineDisks, modTime := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
|
||||
onlineDisks, modTime, _ := listOnlineDisks(erasureDisks, partsMetadata, test.errs, rQuorum)
|
||||
if !modTime.Equal(test.expectedTime) {
|
||||
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
||||
test.expectedTime, modTime)
|
||||
@@ -548,7 +548,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
||||
t.Fatalf("Failed to get quorum consistent fileInfo %v", err)
|
||||
}
|
||||
|
||||
erasureDisks, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
||||
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
||||
|
||||
filteredDisks, errs, _ := disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
||||
|
||||
+92
-79
@@ -23,11 +23,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/sync/errgroup"
|
||||
)
|
||||
@@ -67,6 +67,68 @@ func (fi FileInfo) DataShardFixed() bool {
|
||||
return fi.Metadata[reservedMetadataPrefixLowerDataShardFix] == "true"
|
||||
}
|
||||
|
||||
func (er erasureObjects) listAndHeal(bucket, prefix string, healEntry func(string, metaCacheEntry) error) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
disks, _ := er.getOnlineDisksWithHealing()
|
||||
if len(disks) == 0 {
|
||||
return errors.New("listAndHeal: No non-healing drives found")
|
||||
}
|
||||
|
||||
// How to resolve partial results.
|
||||
resolver := metadataResolutionParams{
|
||||
dirQuorum: 1,
|
||||
objQuorum: 1,
|
||||
bucket: bucket,
|
||||
strict: false, // Allow less strict matching.
|
||||
}
|
||||
|
||||
path := baseDirFromPrefix(prefix)
|
||||
filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
|
||||
if path == prefix {
|
||||
filterPrefix = ""
|
||||
}
|
||||
|
||||
lopts := listPathRawOptions{
|
||||
disks: disks,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
filterPrefix: filterPrefix,
|
||||
recursive: true,
|
||||
forwardTo: "",
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: func(entry metaCacheEntry) {
|
||||
if err := healEntry(bucket, entry); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
// proceed to heal nonetheless.
|
||||
entry, _ = entries.firstFound()
|
||||
}
|
||||
|
||||
if err := healEntry(bucket, *entry); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
},
|
||||
finished: nil,
|
||||
}
|
||||
|
||||
if err := listPathRaw(ctx, lopts); err != nil {
|
||||
return fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HealBucket heals a bucket if it doesn't exist on one of the disks, additionally
|
||||
// also heals the missing entries for bucket metadata files
|
||||
// `policy.json, notification.xml, listeners.json`.
|
||||
@@ -387,11 +449,11 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
|
||||
// List of disks having latest version of the object xl.meta
|
||||
// (by modtime).
|
||||
onlineDisks, modTime := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
||||
onlineDisks, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
||||
|
||||
// Latest FileInfo for reference. If a valid metadata is not
|
||||
// present, it is as good as object not found.
|
||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, readQuorum)
|
||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, etag, readQuorum)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -408,31 +470,12 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
availableDisks, dataErrs, diskMTime := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||
errs, latestMeta, bucket, object, scanMode)
|
||||
|
||||
var (
|
||||
reencode, recreate bool
|
||||
erasureWriter, erasureReader Erasure
|
||||
)
|
||||
|
||||
var erasure Erasure
|
||||
var recreate bool
|
||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||
erasureReader, err = NewErasure(ctx, latestMeta.Erasure.DataBlocks, latestMeta.Erasure.ParityBlocks, latestMeta.Erasure.BlockSize)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
parity := erasureReader.parityBlocks
|
||||
if opts.UpdateParity && bucket != minioMetaBucket {
|
||||
p := globalStorageClass.GetParityForSC(latestMeta.Metadata[xhttp.AmzStorageClass])
|
||||
if p < 0 {
|
||||
return result, fmt.Errorf("invalid parity number: %d", p)
|
||||
}
|
||||
if p != parity {
|
||||
reencode = true
|
||||
parity = p
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize erasure coding
|
||||
erasureWriter, err = NewErasure(ctx, len(storageDisks)-parity, parity, latestMeta.Erasure.BlockSize)
|
||||
erasure, err = NewErasure(ctx, latestMeta.Erasure.DataBlocks,
|
||||
latestMeta.Erasure.ParityBlocks, latestMeta.Erasure.BlockSize)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -444,7 +487,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
recreate = (opts.Recreate &&
|
||||
!latestMeta.InlineData() &&
|
||||
len(latestMeta.Parts) == 1 &&
|
||||
erasureWriter.ShardFileSize(latestMeta.Parts[0].ActualSize) < smallFileThreshold)
|
||||
erasure.ShardFileSize(latestMeta.Parts[0].ActualSize) < smallFileThreshold)
|
||||
}
|
||||
|
||||
result.ObjectSize, err = latestMeta.GetActualSize()
|
||||
@@ -473,7 +516,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
driveState = madmin.DriveStateCorrupt
|
||||
}
|
||||
|
||||
if reencode || shouldHealObjectOnDisk(errs[i], dataErrs[i], partsMetadata[i], latestMeta, recreate) {
|
||||
if shouldHealObjectOnDisk(errs[i], dataErrs[i], partsMetadata[i], latestMeta, recreate) {
|
||||
outDatedDisks[i] = storageDisks[i]
|
||||
disksToHealCount++
|
||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||
@@ -521,10 +564,10 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if !latestMeta.XLV1 && !latestMeta.Deleted && !recreate && !reencode && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||
if !latestMeta.XLV1 && !latestMeta.Deleted && !recreate && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||
// When disk to heal count is greater than parity blocks we should simply error out.
|
||||
err := fmt.Errorf("more drives are expected to heal than parity, returned errors: %v (dataErrs %v) -> %s/%s(%s)", errs, dataErrs, bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
err := fmt.Errorf("(%d > %d) more drives are expected to heal than parity, returned errors: %v (dataErrs %v) -> %s/%s(%s)", disksToHealCount, latestMeta.Erasure.ParityBlocks, errs, dataErrs, bucket, object, versionID)
|
||||
logger.LogOnceIf(ctx, err, "heal-object-count-gt-parity")
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
@@ -548,7 +591,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(availableDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from available disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, availableDisks, bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "heal-object-available-disks")
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
@@ -558,7 +601,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, outDatedDisks, bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "heal-object-outdated-disks")
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
@@ -568,7 +611,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(partsMetadata) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from metadata entries (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
latestMeta.Erasure.Distribution, len(partsMetadata), bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "heal-object-metadata-entries")
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
@@ -608,10 +651,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
inlineBuffers = make([]*bytes.Buffer, len(outDatedDisks))
|
||||
}
|
||||
|
||||
// Heal each part. The healed data will be written
|
||||
// to .minio.sys/tmp/uuid/ which needs to be renamed
|
||||
// later to the final location.
|
||||
|
||||
erasureInfo := latestMeta.Erasure
|
||||
for partIndex := 0; partIndex < len(latestMeta.Parts); partIndex++ {
|
||||
partSize := latestMeta.Parts[partIndex].Size
|
||||
@@ -620,20 +659,17 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
partNumber := latestMeta.Parts[partIndex].Number
|
||||
partIdx := latestMeta.Parts[partIndex].Index
|
||||
partChecksums := latestMeta.Parts[partIndex].Checksums
|
||||
tillOffset := erasure.ShardFileOffset(0, partSize, partSize)
|
||||
readers := make([]io.ReaderAt, len(latestDisks))
|
||||
checksumAlgo := erasureInfo.GetChecksumInfo(partNumber).Algorithm
|
||||
|
||||
origPartErasureSize := erasureReader.ShardFileOffset(0, partSize, partSize)
|
||||
newPartErasureSize := erasureWriter.ShardFileOffset(0, partSize, partSize)
|
||||
|
||||
for i, disk := range latestDisks {
|
||||
if disk == OfflineDisk {
|
||||
continue
|
||||
}
|
||||
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
||||
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, origPartErasureSize, checksumAlgo,
|
||||
checksumInfo.Hash, erasureReader.ShardSize())
|
||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||
checksumInfo.Hash, erasure.ShardSize())
|
||||
}
|
||||
writers := make([]io.Writer, len(outDatedDisks))
|
||||
for i, disk := range outDatedDisks {
|
||||
@@ -642,35 +678,18 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
}
|
||||
partPath := pathJoin(tmpID, dstDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
if len(inlineBuffers) > 0 {
|
||||
inlineBuffers[i] = bytes.NewBuffer(make([]byte, 0, erasureWriter.ShardFileSize(latestMeta.Size)+32))
|
||||
writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasureWriter.ShardSize())
|
||||
inlineBuffers[i] = bytes.NewBuffer(make([]byte, 0, erasure.ShardFileSize(latestMeta.Size)+32))
|
||||
writers[i] = newStreamingBitrotWriterBuffer(inlineBuffers[i], DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
} else {
|
||||
writers[i] = newBitrotWriter(disk, minioMetaTmpBucket, partPath,
|
||||
newPartErasureSize, DefaultBitrotAlgorithm, erasureWriter.ShardSize())
|
||||
tillOffset, DefaultBitrotAlgorithm, erasure.ShardSize())
|
||||
}
|
||||
}
|
||||
|
||||
if reencode {
|
||||
rd, rw := io.Pipe()
|
||||
go func() {
|
||||
n, err := erasureReader.Decode(ctx, rw, readers, 0, partSize, partSize, nil)
|
||||
if err == nil && n != partSize {
|
||||
err = errors.New("unexpected content")
|
||||
}
|
||||
rw.CloseWithError(err)
|
||||
}()
|
||||
buffer := er.bp.Get()
|
||||
n, encodeErr := erasureWriter.Encode(ctx, rd, writers, buffer, erasureWriter.dataBlocks)
|
||||
er.bp.Put(buffer)
|
||||
if encodeErr == nil && n != partSize {
|
||||
encodeErr = errors.New("unexpected content")
|
||||
}
|
||||
rd.CloseWithError(encodeErr)
|
||||
err = encodeErr
|
||||
} else {
|
||||
err = erasureReader.Heal(ctx, writers, readers, partSize)
|
||||
}
|
||||
|
||||
// Heal each part. erasure.Heal() will write the healed
|
||||
// part to .minio/tmp/uuid/ which needs to be renamed
|
||||
// later to the final location.
|
||||
err = erasure.Heal(ctx, writers, readers, partSize)
|
||||
closeBitrotReaders(readers)
|
||||
closeBitrotWriters(writers)
|
||||
if err != nil {
|
||||
@@ -692,13 +711,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
||||
continue
|
||||
}
|
||||
|
||||
if reencode {
|
||||
partsMetadata[i].SetErasureParityUpdated(erasureReader.parityBlocks, erasureWriter.parityBlocks)
|
||||
}
|
||||
|
||||
partsMetadata[i].Erasure.ParityBlocks = erasureWriter.parityBlocks
|
||||
partsMetadata[i].Erasure.DataBlocks = erasureWriter.dataBlocks
|
||||
|
||||
partsMetadata[i].DataDir = dstDataDir
|
||||
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
|
||||
partsMetadata[i].Erasure.AddChecksumInfo(ChecksumInfo{
|
||||
@@ -1063,9 +1075,6 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
notFoundMetaErrs, corruptedMetaErrs, driveNotFoundMetaErrs := danglingErrsCount(errs)
|
||||
notFoundPartsErrs, corruptedPartsErrs, driveNotFoundPartsErrs := danglingErrsCount(ndataErrs)
|
||||
|
||||
if driveNotFoundMetaErrs > 0 || driveNotFoundPartsErrs > 0 {
|
||||
return validMeta, false
|
||||
}
|
||||
for _, m := range metaArr {
|
||||
if m.IsValid() {
|
||||
validMeta = m
|
||||
@@ -1078,6 +1087,10 @@ func isObjectDangling(metaArr []FileInfo, errs []error, dataErrs []error) (valid
|
||||
return validMeta, false
|
||||
}
|
||||
|
||||
if driveNotFoundMetaErrs > 0 || driveNotFoundPartsErrs > 0 {
|
||||
return validMeta, false
|
||||
}
|
||||
|
||||
if validMeta.Deleted {
|
||||
// notFoundPartsErrs is ignored since
|
||||
// - delete marker does not have any parts
|
||||
|
||||
+11
-11
@@ -32,7 +32,7 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
uuid2 "github.com/google/uuid"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
)
|
||||
|
||||
@@ -584,7 +584,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, disks, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
objLayer, disks, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -776,8 +776,8 @@ func TestHealCorrectQuorum(t *testing.T) {
|
||||
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
pools := mustGetPoolEndpoints(fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(fsDirs[16:]...)...)
|
||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, _, err := initObjectLayer(ctx, pools)
|
||||
@@ -907,8 +907,8 @@ func TestHealObjectCorruptedPools(t *testing.T) {
|
||||
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
pools := mustGetPoolEndpoints(fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(fsDirs[16:]...)...)
|
||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, _, err := initObjectLayer(ctx, pools)
|
||||
@@ -1088,7 +1088,7 @@ func TestHealObjectCorruptedXLMeta(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1231,7 +1231,7 @@ func TestHealObjectCorruptedParts(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1388,7 +1388,7 @@ func TestHealObjectErasure(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1486,7 +1486,7 @@ func TestHealEmptyDirectoryErasure(t *testing.T) {
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1582,7 +1582,7 @@ func TestHealLastDataShard(t *testing.T) {
|
||||
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
obj, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func TestShuffleDisks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(disks...))
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, disks...))
|
||||
if err != nil {
|
||||
removeRoots(disks)
|
||||
t.Fatal(err)
|
||||
@@ -204,7 +204,7 @@ func TestEvalDisks(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(disks...))
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, disks...))
|
||||
if err != nil {
|
||||
removeRoots(disks)
|
||||
t.Fatal(err)
|
||||
|
||||
+16
-9
@@ -328,7 +328,7 @@ func (fi FileInfo) ObjectToPartOffset(ctx context.Context, offset int64) (partIn
|
||||
return 0, 0, InvalidRange{}
|
||||
}
|
||||
|
||||
func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.Time, quorum int) (FileInfo, error) {
|
||||
func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.Time, etag string, quorum int) (FileInfo, error) {
|
||||
// with less quorum return error.
|
||||
if quorum < 1 {
|
||||
return FileInfo{}, errErasureReadQuorum
|
||||
@@ -336,12 +336,21 @@ func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.
|
||||
metaHashes := make([]string, len(metaArr))
|
||||
h := sha256.New()
|
||||
for i, meta := range metaArr {
|
||||
if meta.IsValid() && meta.ModTime.Equal(modTime) {
|
||||
if !meta.IsValid() {
|
||||
continue
|
||||
}
|
||||
etagOnly := modTime.Equal(timeSentinel) && (etag != "" && etag == meta.Metadata["etag"])
|
||||
mtimeValid := meta.ModTime.Equal(modTime)
|
||||
if mtimeValid || etagOnly {
|
||||
fmt.Fprintf(h, "%v", meta.XLV1)
|
||||
fmt.Fprintf(h, "%v", meta.GetDataDir())
|
||||
if !etagOnly {
|
||||
// Verify dataDir is same only when mtime is valid and etag is not considered.
|
||||
fmt.Fprintf(h, "%v", meta.GetDataDir())
|
||||
}
|
||||
for _, part := range meta.Parts {
|
||||
fmt.Fprintf(h, "part.%d", part.Number)
|
||||
}
|
||||
fmt.Fprintf(h, "%v+%v", meta.Erasure.DataBlocks, meta.Erasure.ParityBlocks)
|
||||
fmt.Fprintf(h, "%v", meta.Erasure.Distribution)
|
||||
|
||||
// ILM transition fields
|
||||
@@ -408,8 +417,8 @@ func pickValidDiskTimeWithQuorum(metaArr []FileInfo, quorum int) time.Time {
|
||||
|
||||
// pickValidFileInfo - picks one valid FileInfo content and returns from a
|
||||
// slice of FileInfo.
|
||||
func pickValidFileInfo(ctx context.Context, metaArr []FileInfo, modTime time.Time, quorum int) (FileInfo, error) {
|
||||
return findFileInfoInQuorum(ctx, metaArr, modTime, quorum)
|
||||
func pickValidFileInfo(ctx context.Context, metaArr []FileInfo, modTime time.Time, etag string, quorum int) (FileInfo, error) {
|
||||
return findFileInfoInQuorum(ctx, metaArr, modTime, etag, quorum)
|
||||
}
|
||||
|
||||
// writeUniqueFileInfo - writes unique `xl.meta` content for each disk concurrently.
|
||||
@@ -626,11 +635,9 @@ func getInternalReplicationState(m map[string]string) ReplicationState {
|
||||
for k, v := range m {
|
||||
switch {
|
||||
case equals(k, ReservedMetadataPrefixLower+ReplicationTimestamp):
|
||||
tm, _ := time.Parse(http.TimeFormat, v)
|
||||
d.ReplicationTimeStamp = tm
|
||||
d.ReplicaTimeStamp, _ = amztime.ParseReplicationTS(v)
|
||||
case equals(k, ReservedMetadataPrefixLower+ReplicaTimestamp):
|
||||
tm, _ := time.Parse(http.TimeFormat, v)
|
||||
d.ReplicaTimeStamp = tm
|
||||
d.ReplicaTimeStamp, _ = amztime.ParseReplicationTS(v)
|
||||
case equals(k, ReservedMetadataPrefixLower+ReplicaStatus):
|
||||
d.ReplicaStatus = replication.StatusType(v)
|
||||
case equals(k, ReservedMetadataPrefixLower+ReplicationStatus):
|
||||
|
||||
@@ -204,7 +204,7 @@ func TestFindFileInfoInQuorum(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run("", func(t *testing.T) {
|
||||
_, err := findFileInfoInQuorum(context.Background(), test.fis, test.modTime, test.expectedQuorum)
|
||||
_, err := findFileInfoInQuorum(context.Background(), test.fis, test.modTime, "", test.expectedQuorum)
|
||||
if err != test.expectedErr {
|
||||
t.Errorf("Expected %s, got %s", test.expectedErr, err)
|
||||
}
|
||||
|
||||
@@ -83,8 +83,9 @@ func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object
|
||||
if write {
|
||||
quorum = writeQuorum
|
||||
}
|
||||
|
||||
// List all online disks.
|
||||
_, modTime := listOnlineDisks(storageDisks, partsMetadata, errs, quorum)
|
||||
_, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, quorum)
|
||||
|
||||
if write {
|
||||
reducedErr := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||
@@ -98,7 +99,7 @@ func (er erasureObjects) checkUploadIDExists(ctx context.Context, bucket, object
|
||||
}
|
||||
|
||||
// Pick one from the first valid metadata.
|
||||
fi, err = pickValidFileInfo(ctx, partsMetadata, modTime, quorum)
|
||||
fi, err = pickValidFileInfo(ctx, partsMetadata, modTime, etag, quorum)
|
||||
|
||||
return fi, partsMetadata, err
|
||||
}
|
||||
@@ -1248,7 +1249,14 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
}
|
||||
|
||||
if !opts.Speedtest && versionsDisparity {
|
||||
listAndHeal(ctx, bucket, object, &er, healObjectVersionsDisparity)
|
||||
globalMRFState.addPartialOp(partialOperation{
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
queued: time.Now(),
|
||||
allVersions: true,
|
||||
setIndex: er.setIndex,
|
||||
poolIndex: er.poolIndex,
|
||||
})
|
||||
}
|
||||
|
||||
// Check if there is any offline disk and add it to the MRF list
|
||||
|
||||
+75
-26
@@ -32,11 +32,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/readahead"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||
"github.com/minio/minio/internal/bucket/object/lock"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
@@ -107,10 +108,10 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
}
|
||||
|
||||
// List all online disks.
|
||||
onlineDisks, modTime := listOnlineDisks(storageDisks, metaArr, errs, readQuorum)
|
||||
onlineDisks, modTime, etag := listOnlineDisks(storageDisks, metaArr, errs, readQuorum)
|
||||
|
||||
// Pick latest valid metadata.
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, readQuorum)
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
|
||||
if err != nil {
|
||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||
}
|
||||
@@ -346,11 +347,6 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
|
||||
|
||||
var healOnce sync.Once
|
||||
|
||||
// once we have obtained a common FileInfo i.e latest, we should stick
|
||||
// to single dataDir to read the content to avoid reading from some other
|
||||
// dataDir that has stale FileInfo{} to ensure that we fail appropriately
|
||||
// during reads and expect the same dataDir everywhere.
|
||||
dataDir := fi.DataDir
|
||||
for ; partIndex <= lastPartIndex; partIndex++ {
|
||||
if length == totalBytesRead {
|
||||
break
|
||||
@@ -378,8 +374,11 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
|
||||
if !metaArr[index].IsValid() {
|
||||
continue
|
||||
}
|
||||
if !metaArr[index].Erasure.Equal(fi.Erasure) {
|
||||
continue
|
||||
}
|
||||
checksumInfo := metaArr[index].Erasure.GetChecksumInfo(partNumber)
|
||||
partPath := pathJoin(object, dataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
partPath := pathJoin(object, metaArr[index].DataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
readers[index] = newBitrotReader(disk, metaArr[index].Data, bucket, partPath, tillOffset,
|
||||
checksumInfo.Algorithm, checksumInfo.Hash, erasure.ShardSize())
|
||||
|
||||
@@ -528,6 +527,10 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
metadataArray := make([]*xlMetaV2, len(disks))
|
||||
metaFileInfos := make([]FileInfo, len(metadataArray))
|
||||
metadataShallowVersions := make([][]xlMetaV2ShallowVersion, len(disks))
|
||||
var v2bufs [][]byte
|
||||
if !readData {
|
||||
v2bufs = make([][]byte, len(disks))
|
||||
}
|
||||
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
// Read `xl.meta` in parallel across disks.
|
||||
@@ -541,6 +544,10 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !readData {
|
||||
// Save the buffer so we can reuse it.
|
||||
v2bufs[index] = rf.Buf
|
||||
}
|
||||
|
||||
var xl xlMetaV2
|
||||
if err = xl.LoadOrConvert(rf.Buf); err != nil {
|
||||
@@ -624,6 +631,11 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
}
|
||||
metaFileInfos[index].DiskMTime = diskMTime
|
||||
}
|
||||
if !readData {
|
||||
for i := range v2bufs {
|
||||
metaDataPoolPut(v2bufs[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Return all the metadata.
|
||||
return metaFileInfos, errs
|
||||
@@ -663,10 +675,10 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
}
|
||||
|
||||
// List all online disks.
|
||||
onlineDisks, modTime := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
|
||||
// Pick latest valid metadata.
|
||||
fi, err = pickValidFileInfo(ctx, metaArr, modTime, readQuorum)
|
||||
fi, err = pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
|
||||
if err != nil {
|
||||
return fi, nil, nil, err
|
||||
}
|
||||
@@ -674,7 +686,7 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
if !fi.Deleted && len(fi.Erasure.Distribution) != len(onlineDisks) {
|
||||
err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||
fi.Erasure.Distribution, onlineDisks, bucket, object, opts.VersionID)
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "get-object-file-info-manually-modified")
|
||||
return fi, nil, nil, toObjectErr(err, bucket, object, opts.VersionID)
|
||||
}
|
||||
|
||||
@@ -692,9 +704,20 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
missingBlocks++
|
||||
continue
|
||||
}
|
||||
if metaArr[i].IsValid() && metaArr[i].ModTime.Equal(fi.ModTime) {
|
||||
continue
|
||||
}
|
||||
|
||||
// verify metadata is valid, it has similar erasure info
|
||||
// as well as common modtime, if modtime is not possible
|
||||
// verify if it has common "etag" atleast.
|
||||
if metaArr[i].IsValid() && metaArr[i].Erasure.Equal(fi.Erasure) {
|
||||
ok := metaArr[i].ModTime.Equal(modTime)
|
||||
if modTime.IsZero() || modTime.Equal(timeSentinel) {
|
||||
ok = etag != "" && etag == fi.Metadata["etag"]
|
||||
}
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
} // in all other cases metadata is corrupt, do not read from it.
|
||||
|
||||
metaArr[i] = FileInfo{}
|
||||
onlineDisks[i] = nil
|
||||
missingBlocks++
|
||||
@@ -1012,13 +1035,24 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
data := r.Reader
|
||||
|
||||
if opts.CheckPrecondFn != nil {
|
||||
obj, err := er.getObjectInfo(ctx, bucket, object, opts)
|
||||
if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) {
|
||||
return objInfo, err
|
||||
if !opts.NoLock {
|
||||
ns := er.NewNSLock(bucket, object)
|
||||
lkctx, err := ns.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
ctx = lkctx.Context()
|
||||
defer ns.Unlock(lkctx)
|
||||
opts.NoLock = true
|
||||
}
|
||||
if opts.CheckPrecondFn(obj) {
|
||||
|
||||
obj, err := er.getObjectInfo(ctx, bucket, object, opts)
|
||||
if err == nil && opts.CheckPrecondFn(obj) {
|
||||
return objInfo, PreConditionFailed{}
|
||||
}
|
||||
if err != nil && !isErrVersionNotFound(err) && !isErrObjectNotFound(err) && !isErrReadQuorum(err) {
|
||||
return objInfo, err
|
||||
}
|
||||
}
|
||||
|
||||
// Validate input data size and it can never be less than -1.
|
||||
@@ -1263,8 +1297,16 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
}
|
||||
|
||||
userDefined["etag"] = r.MD5CurrentHexString()
|
||||
kind, _ := crypto.IsEncrypted(userDefined)
|
||||
if opts.PreserveETag != "" {
|
||||
userDefined["etag"] = opts.PreserveETag
|
||||
if !opts.ReplicationRequest {
|
||||
userDefined["etag"] = opts.PreserveETag
|
||||
} else if kind != crypto.S3 {
|
||||
// if we have a replication request
|
||||
// and SSE-S3 is specified do not preserve
|
||||
// the incoming etag.
|
||||
userDefined["etag"] = opts.PreserveETag
|
||||
}
|
||||
}
|
||||
|
||||
// Guess content-type from the extension if possible.
|
||||
@@ -1293,7 +1335,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
if errors.Is(err, errFileNotFound) {
|
||||
return ObjectInfo{}, toObjectErr(errErasureWriteQuorum, bucket, object)
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-object-rename"+bucket+object)
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -1320,7 +1362,14 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
}
|
||||
|
||||
if versionsDisparity {
|
||||
listAndHeal(ctx, bucket, object, &er, healObjectVersionsDisparity)
|
||||
globalMRFState.addPartialOp(partialOperation{
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
queued: time.Now(),
|
||||
allVersions: true,
|
||||
setIndex: er.setIndex,
|
||||
poolIndex: er.poolIndex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1785,10 +1834,10 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
}
|
||||
|
||||
// List all online disks.
|
||||
onlineDisks, modTime := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
|
||||
// Pick latest valid metadata.
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, readQuorum)
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
@@ -1858,10 +1907,10 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
||||
}
|
||||
|
||||
// List all online disks.
|
||||
onlineDisks, modTime := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
onlineDisks, modTime, etag := listOnlineDisks(disks, metaArr, errs, readQuorum)
|
||||
|
||||
// Pick latest valid metadata.
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, readQuorum)
|
||||
fi, err := pickValidFileInfo(ctx, metaArr, modTime, etag, readQuorum)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
@@ -1076,7 +1076,7 @@ func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
fsDirs = append(fsDirs, filepath.Join(path, fmt.Sprintf("disk%d", i)))
|
||||
}
|
||||
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(0, fsDirs...))
|
||||
if err != nil {
|
||||
removeRoots(fsDirs)
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/console"
|
||||
@@ -62,10 +62,10 @@ type PoolDecommissionInfo struct {
|
||||
Object string `json:"-" msg:"obj"`
|
||||
|
||||
// Verbose information
|
||||
ItemsDecommissioned int64 `json:"-" msg:"id"`
|
||||
ItemsDecommissionFailed int64 `json:"-" msg:"idf"`
|
||||
BytesDone int64 `json:"-" msg:"bd"`
|
||||
BytesFailed int64 `json:"-" msg:"bf"`
|
||||
ItemsDecommissioned int64 `json:"objectsDecommissioned" msg:"id"`
|
||||
ItemsDecommissionFailed int64 `json:"objectsDecommissionedFailed" msg:"idf"`
|
||||
BytesDone int64 `json:"bytesDecommissioned" msg:"bd"`
|
||||
BytesFailed int64 `json:"bytesDecommissionedFailed" msg:"bf"`
|
||||
}
|
||||
|
||||
// bucketPop should be called when a bucket is done decommissioning.
|
||||
@@ -419,7 +419,9 @@ func (p poolMeta) save(ctx context.Context, pools []*erasureSets) error {
|
||||
// Saves on all pools to make sure decommissioning of first pool is allowed.
|
||||
for i, eset := range pools {
|
||||
if err = saveConfig(ctx, eset, poolMetaName, buf); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("saving pool.bin for pool index %d failed with: %v", i, err))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
logger.LogIf(ctx, fmt.Errorf("saving pool.bin for pool index %d failed with: %v", i, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -459,22 +461,34 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
||||
if !update {
|
||||
z.poolMeta = meta
|
||||
} else {
|
||||
meta = poolMeta{} // to update write poolMeta fresh.
|
||||
newMeta := poolMeta{} // to update write poolMeta fresh.
|
||||
// looks like new pool was added we need to update,
|
||||
// or this is a fresh installation (or an existing
|
||||
// installation with pool removed)
|
||||
meta.Version = poolMetaVersion
|
||||
newMeta.Version = poolMetaVersion
|
||||
for idx, pool := range z.serverPools {
|
||||
meta.Pools = append(meta.Pools, PoolStatus{
|
||||
var skip bool
|
||||
for _, currentPool := range meta.Pools {
|
||||
// Preserve any current pool status.
|
||||
if currentPool.CmdLine == pool.endpoints.CmdLine {
|
||||
newMeta.Pools = append(newMeta.Pools, currentPool)
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
newMeta.Pools = append(newMeta.Pools, PoolStatus{
|
||||
CmdLine: pool.endpoints.CmdLine,
|
||||
ID: idx,
|
||||
LastUpdate: UTCNow(),
|
||||
})
|
||||
}
|
||||
if err = meta.save(ctx, z.serverPools); err != nil {
|
||||
if err = newMeta.save(ctx, z.serverPools); err != nil {
|
||||
return err
|
||||
}
|
||||
z.poolMeta = meta
|
||||
z.poolMeta = newMeta
|
||||
}
|
||||
|
||||
pools := meta.returnResumablePools()
|
||||
@@ -720,18 +734,24 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
// to create the appropriate stack.
|
||||
versionsSorter(fivs.Versions).reverse()
|
||||
|
||||
var decommissionedCount int
|
||||
var decommissioned, expired int
|
||||
for _, version := range fivs.Versions {
|
||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionObject, idx, bi.Name, version.Name, version.VersionID)
|
||||
// Apply lifecycle rules on the objects that are expired.
|
||||
if filterLifecycle(bi.Name, version.Name, version) {
|
||||
decommissionedCount++
|
||||
expired++
|
||||
decommissioned++
|
||||
stopFn(errors.New("ILM expired object/version will be skipped"))
|
||||
continue
|
||||
}
|
||||
|
||||
// any object with only single DEL marker we don't need
|
||||
// to decommission, just skip it.
|
||||
if version.Deleted && len(fivs.Versions) == 1 {
|
||||
decommissionedCount++
|
||||
// to decommission, just skip it, this also includes
|
||||
// any other versions that have already expired.
|
||||
remainingVersions := len(fivs.Versions) - expired
|
||||
if version.Deleted && remainingVersions == 1 {
|
||||
decommissioned++
|
||||
stopFn(errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -756,6 +776,12 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
SkipDecommissioned: true, // make sure we skip the decommissioned pool
|
||||
})
|
||||
var failure bool
|
||||
if err != nil {
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
stopFn(err)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
failure = true
|
||||
@@ -765,7 +791,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
z.poolMetaMutex.Unlock()
|
||||
if !failure {
|
||||
// Success keep a count.
|
||||
decommissionedCount++
|
||||
decommissioned++
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -774,7 +800,6 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
// gr.Close() is ensured by decommissionObject().
|
||||
for try := 0; try < 3; try++ {
|
||||
if version.IsRemote() {
|
||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionObject, idx, bi.Name, version.Name, version.VersionID)
|
||||
if err := z.DecomTieredObject(ctx, bi.Name, version.Name, version, ObjectOptions{
|
||||
VersionID: versionID,
|
||||
MTime: version.ModTime,
|
||||
@@ -789,7 +814,6 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
failure = false
|
||||
break
|
||||
}
|
||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionObject, idx, bi.Name, version.Name, version.VersionID)
|
||||
gr, err := set.GetObjectNInfo(ctx,
|
||||
bi.Name,
|
||||
encodeDirObject(version.Name),
|
||||
@@ -840,11 +864,11 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
if failure {
|
||||
break // break out on first error
|
||||
}
|
||||
decommissionedCount++
|
||||
decommissioned++
|
||||
}
|
||||
|
||||
// if all versions were decommissioned, then we can delete the object versions.
|
||||
if decommissionedCount == len(fivs.Versions) {
|
||||
if decommissioned == len(fivs.Versions) {
|
||||
stopFn := globalDecommissionMetrics.log(decomMetricDecommissionRemoveObject, idx, bi.Name, entry.name)
|
||||
_, err := set.DeleteObject(ctx,
|
||||
bi.Name,
|
||||
@@ -1097,9 +1121,13 @@ func (z *erasureServerPools) Decommission(ctx context.Context, indices ...int) e
|
||||
return err
|
||||
}
|
||||
|
||||
for _, idx := range indices {
|
||||
go z.doDecommissionInRoutine(ctx, idx)
|
||||
}
|
||||
go func() {
|
||||
for _, idx := range indices {
|
||||
// decommission all pools serially one after
|
||||
// the other.
|
||||
z.doDecommissionInRoutine(ctx, idx)
|
||||
}
|
||||
}()
|
||||
|
||||
// Successfully started decommissioning.
|
||||
return nil
|
||||
|
||||
@@ -29,8 +29,8 @@ func prepareErasurePools() (ObjectLayer, []string, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
pools := mustGetPoolEndpoints(fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(fsDirs[16:]...)...)
|
||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||
|
||||
// Everything is fine, should return nil
|
||||
objLayer, err := newErasureServerPools(context.Background(), pools)
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/env"
|
||||
@@ -63,7 +63,10 @@ func (rs *rebalanceStats) update(bucket string, fi FileInfo) {
|
||||
}
|
||||
|
||||
rs.NumVersions++
|
||||
onDiskSz := fi.Size * int64(fi.Erasure.DataBlocks+fi.Erasure.ParityBlocks) / int64(fi.Erasure.DataBlocks)
|
||||
onDiskSz := int64(0)
|
||||
if !fi.Deleted {
|
||||
onDiskSz = fi.Size * int64(fi.Erasure.DataBlocks+fi.Erasure.ParityBlocks) / int64(fi.Erasure.DataBlocks)
|
||||
}
|
||||
rs.Bytes += uint64(onDiskSz)
|
||||
rs.Bucket = bucket
|
||||
rs.Object = fi.Name
|
||||
@@ -497,7 +500,7 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
// to create the appropriate stack.
|
||||
versionsSorter(fivs.Versions).reverse()
|
||||
|
||||
var rebalanced int
|
||||
var rebalanced, expired int
|
||||
for _, version := range fivs.Versions {
|
||||
// Skip transitioned objects for now. TBD
|
||||
if version.IsRemote() {
|
||||
@@ -507,12 +510,15 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
// Apply lifecycle rules on the objects that are expired.
|
||||
if filterLifecycle(bucket, version.Name, version) {
|
||||
rebalanced++
|
||||
expired++
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty delete markers we can assume that they can be purged
|
||||
// as there is no more data associated with them.
|
||||
if version.Deleted && len(fivs.Versions) == 1 {
|
||||
// any object with only single DEL marker we don't need
|
||||
// to rebalance, just skip it, this also includes
|
||||
// any other versions that have already expired.
|
||||
remainingVersions := len(fivs.Versions) - expired
|
||||
if version.Deleted && remainingVersions == 1 {
|
||||
rebalanced++
|
||||
continue
|
||||
}
|
||||
@@ -547,9 +553,10 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
continue
|
||||
}
|
||||
|
||||
var failure bool
|
||||
var failure, ignore bool
|
||||
for try := 0; try < 3; try++ {
|
||||
// GetObjectReader.Close is called by rebalanceObject
|
||||
stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceObject, poolIdx, bucket, version.Name, version.VersionID)
|
||||
gr, err := set.GetObjectNInfo(ctx,
|
||||
bucket,
|
||||
encodeDirObject(version.Name),
|
||||
@@ -562,19 +569,21 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
})
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
// object deleted by the application, nothing to do here we move on.
|
||||
return
|
||||
ignore = true
|
||||
stopFn(nil)
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
failure = true
|
||||
logger.LogIf(ctx, err)
|
||||
stopFn(err)
|
||||
continue
|
||||
}
|
||||
|
||||
stopFn := globalRebalanceMetrics.log(rebalanceMetricRebalanceObject, poolIdx, bucket, version.Name)
|
||||
if err = z.rebalanceObject(ctx, bucket, gr); err != nil {
|
||||
stopFn(err)
|
||||
failure = true
|
||||
logger.LogIf(ctx, err)
|
||||
stopFn(err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -582,7 +591,9 @@ func (z *erasureServerPools) rebalanceBucket(ctx context.Context, bucket string,
|
||||
failure = false
|
||||
break
|
||||
}
|
||||
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
if failure {
|
||||
break // break out on first error
|
||||
}
|
||||
|
||||
+25
-74
@@ -33,7 +33,7 @@ import (
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
@@ -141,6 +141,10 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
||||
}
|
||||
|
||||
z.decommissionCancelers = make([]context.CancelFunc, len(z.serverPools))
|
||||
|
||||
// initialize the object layer.
|
||||
setObjectLayer(z)
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for {
|
||||
err := z.Init(ctx) // Initializes all pools.
|
||||
@@ -159,6 +163,7 @@ func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServ
|
||||
globalLocalDrivesMu.Lock()
|
||||
globalLocalDrives = localDrives
|
||||
defer globalLocalDrivesMu.Unlock()
|
||||
|
||||
return z, nil
|
||||
}
|
||||
|
||||
@@ -411,7 +416,9 @@ func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bu
|
||||
}
|
||||
// do not remove this check as it can lead to inconsistencies
|
||||
// for all callers of bucket replication.
|
||||
opts.VersionID = ""
|
||||
if !opts.MetadataChg {
|
||||
opts.VersionID = ""
|
||||
}
|
||||
pinfo.ObjInfo, pinfo.Err = pool.GetObjectInfo(ctx, bucket, object, opts)
|
||||
poolObjInfos[i] = pinfo
|
||||
}(i, pool, poolOpts[i])
|
||||
@@ -443,7 +450,7 @@ func (z *erasureServerPools) getPoolInfoExistingWithOpts(ctx context.Context, bu
|
||||
// found a pool
|
||||
return pinfo, nil
|
||||
}
|
||||
if isErrReadQuorum(pinfo.Err) {
|
||||
if isErrReadQuorum(pinfo.Err) && !opts.MetadataChg {
|
||||
// read quorum is returned when the object is visibly
|
||||
// present but its unreadable, we simply ask the writes to
|
||||
// schedule to this pool instead. If there is no quorum
|
||||
@@ -943,7 +950,7 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
|
||||
if !isMinioMetaBucketName(bucket) {
|
||||
avail, err := hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()...), data.Size())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-write-quorum")
|
||||
return ObjectInfo{}, toObjectErr(errErasureWriteQuorum)
|
||||
}
|
||||
if !avail {
|
||||
@@ -986,13 +993,10 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
|
||||
return objInfo, err
|
||||
}
|
||||
|
||||
if opts.DeletePrefix {
|
||||
err := z.deletePrefix(ctx, bucket, object)
|
||||
return ObjectInfo{}, err
|
||||
if !opts.DeletePrefix { // DeletePrefix handles dir object encoding differently.
|
||||
object = encodeDirObject(object)
|
||||
}
|
||||
|
||||
object = encodeDirObject(object)
|
||||
|
||||
// Acquire a write lock before deleting the object.
|
||||
lk := z.NewNSLock(bucket, object)
|
||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||
@@ -1002,6 +1006,10 @@ func (z *erasureServerPools) DeleteObject(ctx context.Context, bucket string, ob
|
||||
ctx = lkctx.Context()
|
||||
defer lk.Unlock(lkctx)
|
||||
|
||||
if opts.DeletePrefix {
|
||||
return ObjectInfo{}, z.deletePrefix(ctx, bucket, object)
|
||||
}
|
||||
|
||||
gopts := opts
|
||||
gopts.NoLock = true
|
||||
pinfo, err := z.getPoolInfoExistingWithOpts(ctx, bucket, object, gopts)
|
||||
@@ -1335,7 +1343,7 @@ func (z *erasureServerPools) ListObjects(ctx context.Context, bucket, prefix, ma
|
||||
merged, err := z.listPath(ctx, &opts)
|
||||
if err != nil && err != io.EOF {
|
||||
if !isErrBucketNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-list-objects-path"+bucket)
|
||||
}
|
||||
return loi, err
|
||||
}
|
||||
@@ -1728,7 +1736,7 @@ func (z *erasureServerPools) HealFormat(ctx context.Context, dryRun bool) (madmi
|
||||
for _, pool := range z.serverPools {
|
||||
result, err := pool.HealFormat(ctx, dryRun)
|
||||
if err != nil && !errors.Is(err, errNoHealRequired) {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "erasure-heal-format")
|
||||
continue
|
||||
}
|
||||
// Count errNoHealRequired across all serverPools,
|
||||
@@ -1898,68 +1906,6 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
||||
// HealObjectFn closure function heals the object.
|
||||
type HealObjectFn func(bucket, object, versionID string) error
|
||||
|
||||
func listAndHeal(ctx context.Context, bucket, prefix string, set *erasureObjects, healEntry func(string, metaCacheEntry) error) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
disks, _ := set.getOnlineDisksWithHealing()
|
||||
if len(disks) == 0 {
|
||||
return errors.New("listAndHeal: No non-healing drives found")
|
||||
}
|
||||
|
||||
// How to resolve partial results.
|
||||
resolver := metadataResolutionParams{
|
||||
dirQuorum: 1,
|
||||
objQuorum: 1,
|
||||
bucket: bucket,
|
||||
strict: false, // Allow less strict matching.
|
||||
}
|
||||
|
||||
path := baseDirFromPrefix(prefix)
|
||||
filterPrefix := strings.Trim(strings.TrimPrefix(prefix, path), slashSeparator)
|
||||
if path == prefix {
|
||||
filterPrefix = ""
|
||||
}
|
||||
|
||||
lopts := listPathRawOptions{
|
||||
disks: disks,
|
||||
bucket: bucket,
|
||||
path: path,
|
||||
filterPrefix: filterPrefix,
|
||||
recursive: true,
|
||||
forwardTo: "",
|
||||
minDisks: 1,
|
||||
reportNotFound: false,
|
||||
agreed: func(entry metaCacheEntry) {
|
||||
if err := healEntry(bucket, entry); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, _ []error) {
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if !ok {
|
||||
// check if we can get one entry atleast
|
||||
// proceed to heal nonetheless.
|
||||
entry, _ = entries.firstFound()
|
||||
}
|
||||
|
||||
if err := healEntry(bucket, *entry); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
},
|
||||
finished: nil,
|
||||
}
|
||||
|
||||
if err := listPathRaw(ctx, lopts); err != nil {
|
||||
return fmt.Errorf("listPathRaw returned %w: opts(%#v)", err, lopts)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
|
||||
healEntry := func(bucket string, entry metaCacheEntry) error {
|
||||
if entry.isDir() {
|
||||
@@ -2017,7 +1963,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
||||
go func(idx int, set *erasureObjects) {
|
||||
defer wg.Done()
|
||||
|
||||
errs[idx] = listAndHeal(ctx, bucket, prefix, set, healEntry)
|
||||
errs[idx] = set.listAndHeal(bucket, prefix, healEntry)
|
||||
}(idx, set)
|
||||
}
|
||||
wg.Wait()
|
||||
@@ -2268,6 +2214,7 @@ func (z *erasureServerPools) PutObjectMetadata(ctx context.Context, bucket, obje
|
||||
return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
// We don't know the size here set 1GiB atleast.
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
@@ -2284,6 +2231,8 @@ func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object s
|
||||
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
|
||||
// We don't know the size here set 1GiB atleast.
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
@@ -2300,6 +2249,8 @@ func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, objec
|
||||
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
opts.MetadataChg = true
|
||||
|
||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||
if err != nil {
|
||||
return ObjectInfo{}, err
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ import (
|
||||
"github.com/dchest/siphash"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/google/uuid"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio-go/v7/pkg/tags"
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestNewErasureSets(t *testing.T) {
|
||||
defer os.RemoveAll(disk)
|
||||
}
|
||||
|
||||
endpoints := mustGetNewEndpoints(erasureDisks...)
|
||||
endpoints := mustGetNewEndpoints(0, erasureDisks...)
|
||||
_, _, err := waitForFormatErasure(true, endpoints, 1, 0, 16, "", "")
|
||||
if err != errInvalidArgument {
|
||||
t.Fatalf("Expecting error, got %s", err)
|
||||
|
||||
+4
-4
@@ -28,7 +28,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/bpool"
|
||||
"github.com/minio/minio/internal/dsync"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
@@ -176,7 +176,7 @@ func getDisksInfo(disks []StorageAPI, endpoints []Endpoint) (disksInfo []madmin.
|
||||
g.Go(func() error {
|
||||
diskEndpoint := endpoints[index].String()
|
||||
if disks[index] == OfflineDisk {
|
||||
logger.LogIf(GlobalContext, fmt.Errorf("%s: %s", errDiskNotFound, endpoints[index]))
|
||||
logger.LogOnceIf(GlobalContext, fmt.Errorf("%s: %s", errDiskNotFound, endpoints[index]), "get-disks-info-offline"+diskEndpoint)
|
||||
disksInfo[index] = madmin.Disk{
|
||||
State: diskErrToDriveState(errDiskNotFound),
|
||||
Endpoint: diskEndpoint,
|
||||
@@ -410,7 +410,7 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
if cache.Info.LastUpdate.Equal(lastSave) {
|
||||
continue
|
||||
}
|
||||
logger.LogIf(ctx, cache.save(ctx, er, dataUsageCacheName))
|
||||
logger.LogOnceIf(ctx, cache.save(ctx, er, dataUsageCacheName), "nsscanner-cache-update")
|
||||
updates <- cache.clone()
|
||||
lastSave = cache.Info.LastUpdate
|
||||
case v, ok := <-bucketResults:
|
||||
@@ -418,7 +418,7 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
||||
// Save final state...
|
||||
cache.Info.NextCycle = wantCycle
|
||||
cache.Info.LastUpdate = time.Now()
|
||||
logger.LogIf(ctx, cache.save(ctx, er, dataUsageCacheName))
|
||||
logger.LogOnceIf(ctx, cache.save(ctx, er, dataUsageCacheName), "nsscanner-channel-closed")
|
||||
updates <- cache
|
||||
return
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ func readKeyEtcd(ctx context.Context, client *etcd.Client, key string) ([]byte,
|
||||
defer cancel()
|
||||
resp, err := client.Get(timeoutCtx, key)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "etcd-retrieve-keys")
|
||||
return nil, etcdErrToErr(err, client.Endpoints())
|
||||
}
|
||||
if resp.Count == 0 {
|
||||
|
||||
+15
-31
@@ -35,20 +35,18 @@ import (
|
||||
// EventNotifier - notifies external systems about events in MinIO.
|
||||
type EventNotifier struct {
|
||||
sync.RWMutex
|
||||
targetList *event.TargetList
|
||||
targetResCh chan event.TargetIDResult
|
||||
bucketRulesMap map[string]event.RulesMap
|
||||
bucketRemoteTargetRulesMap map[string]map[event.TargetID]event.RulesMap
|
||||
targetList *event.TargetList
|
||||
targetResCh chan event.TargetIDResult
|
||||
bucketRulesMap map[string]event.RulesMap
|
||||
}
|
||||
|
||||
// NewEventNotifier - creates new event notification object.
|
||||
func NewEventNotifier() *EventNotifier {
|
||||
// targetList/bucketRulesMap/bucketRemoteTargetRulesMap are populated by NotificationSys.InitBucketTargets()
|
||||
return &EventNotifier{
|
||||
targetList: event.NewTargetList(),
|
||||
targetResCh: make(chan event.TargetIDResult),
|
||||
bucketRulesMap: make(map[string]event.RulesMap),
|
||||
bucketRemoteTargetRulesMap: make(map[string]map[event.TargetID]event.RulesMap),
|
||||
targetList: event.NewTargetList(),
|
||||
targetResCh: make(chan event.TargetIDResult),
|
||||
bucketRulesMap: make(map[string]event.RulesMap),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +120,6 @@ func (evnot *EventNotifier) AddRulesMap(bucketName string, rulesMap event.RulesM
|
||||
|
||||
rulesMap = rulesMap.Clone()
|
||||
|
||||
for _, targetRulesMap := range evnot.bucketRemoteTargetRulesMap[bucketName] {
|
||||
rulesMap.Add(targetRulesMap)
|
||||
}
|
||||
|
||||
// Do not add for an empty rulesMap.
|
||||
if len(rulesMap) == 0 {
|
||||
delete(evnot.bucketRulesMap, bucketName)
|
||||
@@ -174,29 +168,18 @@ func (evnot *EventNotifier) RemoveNotification(bucketName string) {
|
||||
defer evnot.Unlock()
|
||||
|
||||
delete(evnot.bucketRulesMap, bucketName)
|
||||
|
||||
targetIDSet := event.NewTargetIDSet()
|
||||
for targetID := range evnot.bucketRemoteTargetRulesMap[bucketName] {
|
||||
targetIDSet[targetID] = struct{}{}
|
||||
delete(evnot.bucketRemoteTargetRulesMap[bucketName], targetID)
|
||||
}
|
||||
evnot.targetList.Remove(targetIDSet)
|
||||
|
||||
delete(evnot.bucketRemoteTargetRulesMap, bucketName)
|
||||
}
|
||||
|
||||
// RemoveAllRemoteTargets - closes and removes all notification targets.
|
||||
func (evnot *EventNotifier) RemoveAllRemoteTargets() {
|
||||
// RemoveAllBucketTargets - closes and removes all notification targets.
|
||||
func (evnot *EventNotifier) RemoveAllBucketTargets() {
|
||||
evnot.Lock()
|
||||
defer evnot.Unlock()
|
||||
|
||||
for _, targetMap := range evnot.bucketRemoteTargetRulesMap {
|
||||
targetIDSet := event.NewTargetIDSet()
|
||||
for k := range targetMap {
|
||||
targetIDSet[k] = struct{}{}
|
||||
}
|
||||
evnot.targetList.Remove(targetIDSet)
|
||||
targetIDSet := event.NewTargetIDSet()
|
||||
for k := range evnot.targetList.TargetMap() {
|
||||
targetIDSet[k] = struct{}{}
|
||||
}
|
||||
evnot.targetList.Remove(targetIDSet)
|
||||
}
|
||||
|
||||
// Send - sends the event to all registered notification targets
|
||||
@@ -209,7 +192,8 @@ func (evnot *EventNotifier) Send(args eventArgs) {
|
||||
return
|
||||
}
|
||||
|
||||
evnot.targetList.Send(args.ToEvent(true), targetIDSet, evnot.targetResCh)
|
||||
// If MINIO_API_SYNC_EVENTS is set, send events synchronously.
|
||||
evnot.targetList.Send(args.ToEvent(true), targetIDSet, evnot.targetResCh, globalAPIConfig.isSyncEventsEnabled())
|
||||
}
|
||||
|
||||
type eventArgs struct {
|
||||
@@ -284,7 +268,7 @@ func (args eventArgs) ToEvent(escape bool) event.Event {
|
||||
newEvent.S3.Object.ContentType = args.Object.ContentType
|
||||
newEvent.S3.Object.UserMetadata = make(map[string]string, len(args.Object.UserDefined))
|
||||
for k, v := range args.Object.UserDefined {
|
||||
if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(strings.ToLower(k), ReservedMetadataPrefixLower) {
|
||||
continue
|
||||
}
|
||||
newEvent.S3.Object.UserMetadata[k] = v
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestFixFormatV3(t *testing.T) {
|
||||
for _, erasureDir := range erasureDirs {
|
||||
defer os.RemoveAll(erasureDir)
|
||||
}
|
||||
endpoints := mustGetNewEndpoints(erasureDirs...)
|
||||
endpoints := mustGetNewEndpoints(0, erasureDirs...)
|
||||
|
||||
storageDisks, errs := initStorageDisksWithErrors(endpoints, true)
|
||||
for _, err := range errs {
|
||||
@@ -555,7 +555,7 @@ func benchmarkInitStorageDisksN(b *testing.B, nDisks int) {
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
endpoints := mustGetNewEndpoints(fsDirs...)
|
||||
endpoints := mustGetNewEndpoints(0, fsDirs...)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
endpoints := endpoints
|
||||
for pb.Next() {
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
@@ -253,11 +253,7 @@ func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string)
|
||||
return false, err
|
||||
}
|
||||
ldapPolicies, _ := globalIAMSys.PolicyDBGet(ldapUserDN, false, groupDistNames...)
|
||||
if len(ldapPolicies) == 0 {
|
||||
// no policy associated reject it.
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
return len(ldapPolicies) > 0, nil
|
||||
}
|
||||
|
||||
ui, ok := globalIAMSys.GetUser(context.Background(), username)
|
||||
@@ -326,7 +322,7 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||
Secure: globalIsTLS,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
Transport: globalRemoteFTPClientTransport,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -340,7 +336,7 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
||||
return minio.New(driver.endpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||||
Secure: globalIsTLS,
|
||||
Transport: globalRemoteTargetTransport,
|
||||
Transport: globalRemoteFTPClientTransport,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -362,12 +358,20 @@ func (driver *ftpDriver) DeleteDir(ctx *ftp.Context, path string) (err error) {
|
||||
cctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
if prefix == "" {
|
||||
// if all objects are not deleted yet this call may fail.
|
||||
return clnt.RemoveBucket(cctx, bucket)
|
||||
}
|
||||
|
||||
objectsCh := make(chan minio.ObjectInfo)
|
||||
|
||||
// Send object names that are needed to be removed to objectsCh
|
||||
go func() {
|
||||
defer close(objectsCh)
|
||||
opts := minio.ListObjectsOptions{Prefix: prefix, Recursive: true}
|
||||
opts := minio.ListObjectsOptions{
|
||||
Prefix: prefix,
|
||||
Recursive: true,
|
||||
}
|
||||
for object := range clnt.ListObjects(cctx, bucket, opts) {
|
||||
if object.Err != nil {
|
||||
return
|
||||
@@ -427,6 +431,10 @@ func (driver *ftpDriver) MakeDir(ctx *ftp.Context, path string) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if prefix == "" {
|
||||
return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region})
|
||||
}
|
||||
|
||||
dirPath := buildMinioDir(prefix)
|
||||
|
||||
_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0,
|
||||
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var globalRemoteFTPClientTransport = NewRemoteTargetHTTPTransport(true)()
|
||||
|
||||
// minioLogger use an instance of this to log in a standard format
|
||||
type minioLogger struct{}
|
||||
|
||||
@@ -129,10 +131,29 @@ func startSFTPServer(c *cli.Context) {
|
||||
// certificate details and handles authentication of ServerConns.
|
||||
config := &ssh.ServerConfig{
|
||||
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||
if globalIAMSys.LDAPConfig.Enabled() {
|
||||
targetUser, targetGroups, err := globalIAMSys.LDAPConfig.Bind(c.User(), string(pass))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ldapPolicies, _ := globalIAMSys.PolicyDBGet(targetUser, false, targetGroups...)
|
||||
if len(ldapPolicies) == 0 {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
return &ssh.Permissions{
|
||||
CriticalOptions: map[string]string{
|
||||
ldapUser: targetUser,
|
||||
ldapUserN: c.User(),
|
||||
},
|
||||
Extensions: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
ui, ok := globalIAMSys.GetUser(context.Background(), c.User())
|
||||
if !ok {
|
||||
return nil, errNoSuchUser
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(ui.Credentials.SecretKey), pass) == 1 {
|
||||
return &ssh.Permissions{
|
||||
CriticalOptions: map[string]string{
|
||||
|
||||
@@ -71,7 +71,7 @@ const (
|
||||
// and must not set by clients
|
||||
func containsReservedMetadata(header http.Header) bool {
|
||||
for key := range header {
|
||||
if strings.HasPrefix(strings.ToLower(key), ReservedMetadataPrefixLower) {
|
||||
if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func isHTTPHeaderSizeTooLarge(header http.Header) bool {
|
||||
length := len(key) + len(header.Get(key))
|
||||
size += length
|
||||
for _, prefix := range userMetadataKeyPrefixes {
|
||||
if strings.HasPrefix(strings.ToLower(key), prefix) {
|
||||
if stringsHasPrefixFold(key, prefix) {
|
||||
usersize += length
|
||||
break
|
||||
}
|
||||
@@ -499,7 +499,6 @@ func addCustomHeaders(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
header := w.Header()
|
||||
header.Set("X-XSS-Protection", "1; mode=block") // Prevents against XSS attacks
|
||||
header.Set("Content-Security-Policy", "block-all-mixed-content") // prevent mixed (HTTP / HTTPS content)
|
||||
header.Set("X-Content-Type-Options", "nosniff") // Prevent mime-sniff
|
||||
header.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") // HSTS mitigates variants of MITM attacks
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
|
||||
+9
-6
@@ -27,14 +27,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/console/restapi"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/dnscache"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/bucket/bandwidth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/rs/dnscache"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
@@ -224,12 +224,15 @@ var (
|
||||
// registered listeners
|
||||
globalConsoleSys *HTTPConsoleLoggerSys
|
||||
|
||||
// All unique drives for this deployment
|
||||
globalEndpoints EndpointServerPools
|
||||
// All unique nodes for this deployment
|
||||
globalNodes []Node
|
||||
|
||||
// The name of this local node, fetched from arguments
|
||||
globalLocalNodeName string
|
||||
globalLocalNodeNameHex string
|
||||
globalNodeNamesHex map[string]struct{}
|
||||
globalNodeNamesHex = make(map[string]struct{})
|
||||
|
||||
// The global subnet config
|
||||
globalSubnetConfig subnet.Config
|
||||
@@ -237,16 +240,15 @@ var (
|
||||
// The global callhome config
|
||||
globalCallhomeConfig callhome.Config
|
||||
|
||||
globalRemoteEndpoints map[string]Endpoint
|
||||
|
||||
// Global server's network statistics
|
||||
globalConnStats = newConnStats()
|
||||
|
||||
// Global HTTP request statisitics
|
||||
globalHTTPStats = newHTTPStats()
|
||||
|
||||
// Global bucket network statistics
|
||||
// Global bucket network and API statistics
|
||||
globalBucketConnStats = newBucketConnStats()
|
||||
globalBucketHTTPStats = newBucketHTTPStats()
|
||||
|
||||
// Time when the server is started
|
||||
globalBootTime = UTCNow()
|
||||
@@ -368,6 +370,7 @@ var (
|
||||
// Used for collecting stats for netperf
|
||||
globalNetPerfMinDuration = time.Second * 10
|
||||
globalNetPerfRX netPerfRX
|
||||
globalSiteNetPerfRX netPerfRX
|
||||
globalObjectPerfBucket = "minio-perf-test-tmp-bucket"
|
||||
globalObjectPerfUserMetadata = "X-Amz-Meta-Minio-Object-Perf" // Clients can set this to bypass S3 API service freeze. Used by object pref tests.
|
||||
|
||||
|
||||
+10
-1
@@ -52,6 +52,7 @@ type apiConfig struct {
|
||||
disableODirect bool
|
||||
gzipObjects bool
|
||||
rootAccess bool
|
||||
syncEvents bool
|
||||
}
|
||||
|
||||
const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
||||
@@ -166,6 +167,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||
t.disableODirect = cfg.DisableODirect
|
||||
t.gzipObjects = cfg.GzipObjects
|
||||
t.rootAccess = cfg.RootAccess
|
||||
t.syncEvents = cfg.SyncEvents
|
||||
}
|
||||
|
||||
func (t *apiConfig) isDisableODirect() bool {
|
||||
@@ -317,7 +319,7 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
||||
case <-deadlineTimer.C:
|
||||
// Send a http timeout message
|
||||
writeErrorResponse(r.Context(), w,
|
||||
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||
errorCodes.ToAPIErr(ErrTooManyRequests),
|
||||
r.URL)
|
||||
globalHTTPStats.addRequestsInQueue(-1)
|
||||
return
|
||||
@@ -353,3 +355,10 @@ func (t *apiConfig) getTransitionWorkers() int {
|
||||
|
||||
return t.transitionWorkers
|
||||
}
|
||||
|
||||
func (t *apiConfig) isSyncEventsEnabled() bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
return t.syncEvents
|
||||
}
|
||||
|
||||
+21
-10
@@ -26,7 +26,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
@@ -48,7 +48,7 @@ func parseLocationConstraint(r *http.Request) (location string, s3Error APIError
|
||||
locationConstraint := createBucketLocationConfiguration{}
|
||||
err := xmlDecoder(r.Body, &locationConstraint, r.ContentLength)
|
||||
if err != nil && r.ContentLength != 0 {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
logger.LogOnceIf(GlobalContext, err, "location-constraint-xml-parsing")
|
||||
// Treat all other failures as XML parsing errors.
|
||||
return "", ErrMalformedXML
|
||||
} // else for both err as nil or io.EOF
|
||||
@@ -184,7 +184,7 @@ func extractMetadataFromMime(ctx context.Context, v textproto.MIMEHeader, m map[
|
||||
|
||||
for key := range v {
|
||||
for _, prefix := range userMetadataKeyPrefixes {
|
||||
if !strings.HasPrefix(strings.ToLower(key), strings.ToLower(prefix)) {
|
||||
if !stringsHasPrefixFold(key, prefix) {
|
||||
continue
|
||||
}
|
||||
value, ok := nv[http.CanonicalHeaderKey(key)]
|
||||
@@ -269,9 +269,26 @@ func trimAwsChunkedContentEncoding(contentEnc string) (trimmedContentEnc string)
|
||||
|
||||
func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
|
||||
if err != nil {
|
||||
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
apiErr := errorCodes.ToAPIErr(ErrUnsupportedHostHeader)
|
||||
apiErr.Description = fmt.Sprintf("%s: %v", apiErr.Description, err)
|
||||
|
||||
writeErrorResponse(r.Context(), w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucket, _ := path2BucketObject(resource)
|
||||
|
||||
globalHTTPStats.currentS3Requests.Inc(api)
|
||||
defer globalHTTPStats.currentS3Requests.Dec(api)
|
||||
|
||||
if bucket != "" && bucket != minioReservedBucket {
|
||||
globalBucketHTTPStats.updateHTTPStats(bucket, api, nil)
|
||||
}
|
||||
|
||||
f.ServeHTTP(w, r)
|
||||
|
||||
tc, ok := r.Context().Value(mcontext.ContextTraceKey).(*mcontext.TraceCtxt)
|
||||
@@ -299,16 +316,10 @@ func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
|
||||
globalConnStats.incS3InputBytes(int64(tc.RequestRecorder.Size()))
|
||||
globalConnStats.incS3OutputBytes(int64(tc.ResponseRecorder.Size()))
|
||||
|
||||
resource, err := getResource(r.URL.Path, r.Host, globalDomainNames)
|
||||
if err != nil {
|
||||
logger.LogIf(r.Context(), fmt.Errorf("Unable to get the actual resource in the incoming request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
bucket, _ := path2BucketObject(resource)
|
||||
if bucket != "" && bucket != minioReservedBucket {
|
||||
globalBucketConnStats.incS3InputBytes(bucket, int64(tc.RequestRecorder.Size()))
|
||||
globalBucketConnStats.incS3OutputBytes(bucket, int64(tc.ResponseRecorder.Size()))
|
||||
globalBucketHTTPStats.updateHTTPStats(bucket, api, tc.ResponseRecorder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+123
-2
@@ -120,6 +120,105 @@ type bucketS3RXTX struct {
|
||||
s3OutputBytes uint64
|
||||
}
|
||||
|
||||
type bucketHTTPAPIStats struct {
|
||||
currentS3Requests *HTTPAPIStats
|
||||
totalS3Requests *HTTPAPIStats
|
||||
totalS34xxErrors *HTTPAPIStats
|
||||
totalS35xxErrors *HTTPAPIStats
|
||||
totalS3Canceled *HTTPAPIStats
|
||||
}
|
||||
|
||||
type bucketHTTPStats struct {
|
||||
sync.RWMutex
|
||||
httpStats map[string]bucketHTTPAPIStats
|
||||
}
|
||||
|
||||
func newBucketHTTPStats() *bucketHTTPStats {
|
||||
return &bucketHTTPStats{
|
||||
httpStats: make(map[string]bucketHTTPAPIStats),
|
||||
}
|
||||
}
|
||||
|
||||
func (bh *bucketHTTPStats) delete(bucket string) {
|
||||
bh.Lock()
|
||||
defer bh.Unlock()
|
||||
|
||||
delete(bh.httpStats, bucket)
|
||||
}
|
||||
|
||||
func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.ResponseRecorder) {
|
||||
if bh == nil {
|
||||
return
|
||||
}
|
||||
|
||||
bh.Lock()
|
||||
defer bh.Unlock()
|
||||
|
||||
hstats, ok := bh.httpStats[bucket]
|
||||
if !ok {
|
||||
hstats = bucketHTTPAPIStats{
|
||||
currentS3Requests: &HTTPAPIStats{},
|
||||
totalS3Requests: &HTTPAPIStats{},
|
||||
totalS3Canceled: &HTTPAPIStats{},
|
||||
totalS34xxErrors: &HTTPAPIStats{},
|
||||
totalS35xxErrors: &HTTPAPIStats{},
|
||||
}
|
||||
}
|
||||
|
||||
if w == nil { // when response recorder nil, this is an active request
|
||||
hstats.currentS3Requests.Inc(api)
|
||||
bh.httpStats[bucket] = hstats
|
||||
return
|
||||
} // else {
|
||||
hstats.currentS3Requests.Dec(api) // decrement this once we have the response recorder.
|
||||
|
||||
hstats.totalS3Requests.Inc(api)
|
||||
code := w.StatusCode
|
||||
|
||||
switch {
|
||||
case code == 0:
|
||||
case code == 499:
|
||||
// 499 is a good error, shall be counted as canceled.
|
||||
hstats.totalS3Canceled.Inc(api)
|
||||
case code >= http.StatusBadRequest:
|
||||
if code >= http.StatusInternalServerError {
|
||||
hstats.totalS35xxErrors.Inc(api)
|
||||
} else {
|
||||
hstats.totalS34xxErrors.Inc(api)
|
||||
}
|
||||
}
|
||||
|
||||
bh.httpStats[bucket] = hstats
|
||||
}
|
||||
|
||||
func (bh *bucketHTTPStats) load(bucket string) bucketHTTPAPIStats {
|
||||
if bh == nil {
|
||||
return bucketHTTPAPIStats{
|
||||
currentS3Requests: &HTTPAPIStats{},
|
||||
totalS3Requests: &HTTPAPIStats{},
|
||||
totalS3Canceled: &HTTPAPIStats{},
|
||||
totalS34xxErrors: &HTTPAPIStats{},
|
||||
totalS35xxErrors: &HTTPAPIStats{},
|
||||
}
|
||||
}
|
||||
|
||||
bh.RLock()
|
||||
defer bh.RUnlock()
|
||||
|
||||
val, ok := bh.httpStats[bucket]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return bucketHTTPAPIStats{
|
||||
currentS3Requests: &HTTPAPIStats{},
|
||||
totalS3Requests: &HTTPAPIStats{},
|
||||
totalS3Canceled: &HTTPAPIStats{},
|
||||
totalS34xxErrors: &HTTPAPIStats{},
|
||||
totalS35xxErrors: &HTTPAPIStats{},
|
||||
}
|
||||
}
|
||||
|
||||
type bucketConnStats struct {
|
||||
sync.RWMutex
|
||||
stats map[string]*bucketS3RXTX
|
||||
@@ -225,10 +324,32 @@ func (stats *HTTPAPIStats) Dec(api string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the current counter on input API string
|
||||
func (stats *HTTPAPIStats) Get(api string) int {
|
||||
if stats == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
stats.RLock()
|
||||
defer stats.RUnlock()
|
||||
|
||||
val, ok := stats.apiStats[api]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Load returns the recorded stats.
|
||||
func (stats *HTTPAPIStats) Load() map[string]int {
|
||||
stats.Lock()
|
||||
defer stats.Unlock()
|
||||
if stats == nil {
|
||||
return map[string]int{}
|
||||
}
|
||||
|
||||
stats.RLock()
|
||||
defer stats.RUnlock()
|
||||
|
||||
apiStats := make(map[string]int, len(stats.apiStats))
|
||||
for k, v := range stats.apiStats {
|
||||
apiStats[k] = v
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/mcontext"
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
|
||||
+31
-33
@@ -28,7 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
@@ -914,11 +914,12 @@ func (store *IAMStoreSys) listGroups(ctx context.Context) (res []string, err err
|
||||
// PolicyDBUpdate - adds or removes given policies to/from the user or group's
|
||||
// policy associations.
|
||||
func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGroup bool,
|
||||
userType IAMUserType, policies []string, isAttach bool) (updatedAt time.Time, addedOrRemoved []string,
|
||||
err error,
|
||||
userType IAMUserType, policies []string, isAttach bool) (updatedAt time.Time,
|
||||
addedOrRemoved, effectivePolicies []string, err error,
|
||||
) {
|
||||
if name == "" {
|
||||
return updatedAt, nil, errInvalidArgument
|
||||
err = errInvalidArgument
|
||||
return
|
||||
}
|
||||
|
||||
cache := store.lock()
|
||||
@@ -932,11 +933,13 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
if store.getUsersSysType() == MinIOUsersSysType {
|
||||
g, ok := cache.iamGroupsMap[name]
|
||||
if !ok {
|
||||
return updatedAt, nil, errNoSuchGroup
|
||||
err = errNoSuchGroup
|
||||
return
|
||||
}
|
||||
|
||||
if g.Status == statusDisabled {
|
||||
return updatedAt, nil, errGroupDisabled
|
||||
err = errGroupDisabled
|
||||
return
|
||||
}
|
||||
}
|
||||
mp = cache.iamGroupPolicyMap[name]
|
||||
@@ -945,6 +948,7 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
// Compute net policy change effect and updated policy mapping
|
||||
existingPolicySet := mp.policySet()
|
||||
policiesToUpdate := set.CreateStringSet(policies...)
|
||||
var newPolicySet set.StringSet
|
||||
newPolicyMapping := mp
|
||||
if isAttach {
|
||||
// new policies to attach => inputPolicies - existing (set difference)
|
||||
@@ -952,26 +956,29 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
// validate that new policies to add are defined.
|
||||
for _, p := range policiesToUpdate.ToSlice() {
|
||||
if _, found := cache.iamPolicyDocsMap[p]; !found {
|
||||
return updatedAt, nil, errNoSuchPolicy
|
||||
err = errNoSuchPolicy
|
||||
return
|
||||
}
|
||||
}
|
||||
newPolicyMapping.Policies = strings.Join(existingPolicySet.Union(policiesToUpdate).ToSlice(), ",")
|
||||
newPolicySet = existingPolicySet.Union(policiesToUpdate)
|
||||
} else {
|
||||
// policies to detach => inputPolicies ∩ existing (intersection)
|
||||
policiesToUpdate = policiesToUpdate.Intersection(existingPolicySet)
|
||||
newPolicyMapping.Policies = strings.Join(existingPolicySet.Difference(policiesToUpdate).ToSlice(), ",")
|
||||
newPolicySet = existingPolicySet.Difference(policiesToUpdate)
|
||||
}
|
||||
newPolicyMapping.UpdatedAt = UTCNow()
|
||||
|
||||
// We return an error if the requested policy update will have no effect.
|
||||
if policiesToUpdate.IsEmpty() {
|
||||
return updatedAt, nil, errNoPolicyToAttachOrDetach
|
||||
err = errNoPolicyToAttachOrDetach
|
||||
return
|
||||
}
|
||||
|
||||
newPolicies := newPolicySet.ToSlice()
|
||||
newPolicyMapping.Policies = strings.Join(newPolicies, ",")
|
||||
newPolicyMapping.UpdatedAt = UTCNow()
|
||||
addedOrRemoved = policiesToUpdate.ToSlice()
|
||||
|
||||
if err := store.saveMappedPolicy(ctx, name, userType, isGroup, newPolicyMapping); err != nil {
|
||||
return updatedAt, addedOrRemoved, err
|
||||
if err = store.saveMappedPolicy(ctx, name, userType, isGroup, newPolicyMapping); err != nil {
|
||||
return
|
||||
}
|
||||
if !isGroup {
|
||||
cache.iamUserPolicyMap[name] = newPolicyMapping
|
||||
@@ -979,7 +986,7 @@ func (store *IAMStoreSys) PolicyDBUpdate(ctx context.Context, name string, isGro
|
||||
cache.iamGroupPolicyMap[name] = newPolicyMapping
|
||||
}
|
||||
cache.updatedAt = UTCNow()
|
||||
return cache.updatedAt, addedOrRemoved, nil
|
||||
return cache.updatedAt, addedOrRemoved, newPolicies, nil
|
||||
}
|
||||
|
||||
// PolicyDBSet - update the policy mapping for the given user or group in
|
||||
@@ -1451,23 +1458,6 @@ func (store *IAMStoreSys) GetUserInfo(name string) (u madmin.UserInfo, err error
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserPolicies - returns the policies attached to a user.
|
||||
func (store *IAMStoreSys) GetUserPolicies(name string) ([]string, error) {
|
||||
if name == "" {
|
||||
return nil, errInvalidArgument
|
||||
}
|
||||
|
||||
cache := store.rlock()
|
||||
defer store.runlock()
|
||||
|
||||
if cache.iamUserPolicyMap[name].Policies == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
policies := cache.iamUserPolicyMap[name].toSlice()
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// PolicyMappingNotificationHandler - handles updating a policy mapping from storage.
|
||||
func (store *IAMStoreSys) PolicyMappingNotificationHandler(ctx context.Context, userOrGroup string, isGroup bool, userType IAMUserType) error {
|
||||
if userOrGroup == "" {
|
||||
@@ -2187,7 +2177,9 @@ func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey str
|
||||
}
|
||||
}
|
||||
|
||||
if !userExists {
|
||||
// If root user has no STS/Service Accounts, userExists would be false here,
|
||||
// so we handle this exception.
|
||||
if !userExists && globalActiveCred.AccessKey != accessKey {
|
||||
return nil, errNoSuchUser
|
||||
}
|
||||
|
||||
@@ -2343,11 +2335,17 @@ func extractJWTClaims(u UserIdentity) (*jwt.MapClaims, error) {
|
||||
}
|
||||
|
||||
func validateSvcExpirationInUTC(expirationInUTC time.Time) error {
|
||||
if expirationInUTC.IsZero() || expirationInUTC.Equal(timeSentinel) {
|
||||
// Service accounts might not have expiration in older releases.
|
||||
return nil
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC()
|
||||
minExpiration := currentTime.Add(minServiceAccountExpiry)
|
||||
maxExpiration := currentTime.Add(maxServiceAccountExpiry)
|
||||
if expirationInUTC.Before(minExpiration) || expirationInUTC.After(maxExpiration) {
|
||||
return errInvalidSvcAcctExpiration
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+114
-21
@@ -32,7 +32,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/arn"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
@@ -532,6 +532,14 @@ func (sys *IAMSys) DeletePolicy(ctx context.Context, policyName string, notifyPe
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
for _, v := range iampolicy.DefaultPolicies {
|
||||
if v.Name == policyName {
|
||||
if err := checkConfig(ctx, globalObjectAPI, getPolicyDocPath(policyName)); err != nil && err == errConfigNotFound {
|
||||
return fmt.Errorf("inbuilt policy `%s` not allowed to be deleted", policyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := sys.store.DeletePolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -805,7 +813,7 @@ func (sys *IAMSys) QueryLDAPPolicyEntities(ctx context.Context, q madmin.PolicyE
|
||||
}
|
||||
}
|
||||
|
||||
// IsTempUser - returns if given key is a temporary user.
|
||||
// IsTempUser - returns if given key is a temporary user and parent user.
|
||||
func (sys *IAMSys) IsTempUser(name string) (bool, string, error) {
|
||||
if !sys.Initialized() {
|
||||
return false, "", errServerNotInitialized
|
||||
@@ -881,15 +889,6 @@ func (sys *IAMSys) QueryPolicyEntities(ctx context.Context, q madmin.PolicyEntit
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserPolicies - get policies attached to a user.
|
||||
func (sys *IAMSys) GetUserPolicies(name string) (p []string, err error) {
|
||||
if !sys.Initialized() {
|
||||
return p, errServerNotInitialized
|
||||
}
|
||||
|
||||
return sys.store.GetUserPolicies(name)
|
||||
}
|
||||
|
||||
// SetUserStatus - sets current user status, supports disabled or enabled.
|
||||
func (sys *IAMSys) SetUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) (updatedAt time.Time, err error) {
|
||||
if !sys.Initialized() {
|
||||
@@ -1574,13 +1573,93 @@ func (sys *IAMSys) PolicyDBSet(ctx context.Context, name, policy string, userTyp
|
||||
return updatedAt, nil
|
||||
}
|
||||
|
||||
// PolicyDBUpdateBuiltin - adds or removes policies from a user or a group
|
||||
// verified to be an internal IDP user.
|
||||
func (sys *IAMSys) PolicyDBUpdateBuiltin(ctx context.Context, isAttach bool,
|
||||
r madmin.PolicyAssociationReq,
|
||||
) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
|
||||
if !sys.Initialized() {
|
||||
err = errServerNotInitialized
|
||||
return
|
||||
}
|
||||
|
||||
userOrGroup := r.User
|
||||
var isGroup bool
|
||||
if userOrGroup == "" {
|
||||
isGroup = true
|
||||
userOrGroup = r.Group
|
||||
}
|
||||
|
||||
if isGroup {
|
||||
_, err = sys.GetGroupDescription(userOrGroup)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var isTemp bool
|
||||
isTemp, _, err = sys.IsTempUser(userOrGroup)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
return
|
||||
}
|
||||
if isTemp {
|
||||
err = errIAMActionNotAllowed
|
||||
return
|
||||
}
|
||||
|
||||
// When the user is root credential you are not allowed to
|
||||
// add policies for root user.
|
||||
if userOrGroup == globalActiveCred.AccessKey {
|
||||
err = errIAMActionNotAllowed
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that user exists.
|
||||
var userExists bool
|
||||
_, userExists = sys.GetUser(ctx, userOrGroup)
|
||||
if !userExists {
|
||||
err = errNoSuchUser
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(ctx, userOrGroup, isGroup,
|
||||
regUser, r.Policies, isAttach)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other MinIO peers to reload policy
|
||||
if !sys.HasWatcher() {
|
||||
for _, nerr := range globalNotificationSys.LoadPolicyMapping(userOrGroup, regUser, isGroup) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: userOrGroup,
|
||||
UserType: int(regUser),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(effectivePolicies, ","),
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PolicyDBUpdateLDAP - adds or removes policies from a user or a group verified
|
||||
// to be in the LDAP directory.
|
||||
func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
||||
r madmin.PolicyAssociationReq,
|
||||
) (updatedAt time.Time, addedOrRemoved []string, err error) {
|
||||
) (updatedAt time.Time, addedOrRemoved, effectivePolicies []string, err error) {
|
||||
if !sys.Initialized() {
|
||||
return updatedAt, nil, errServerNotInitialized
|
||||
err = errServerNotInitialized
|
||||
return
|
||||
}
|
||||
|
||||
var dn string
|
||||
@@ -1589,28 +1668,31 @@ func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
||||
dn, err = sys.LDAPConfig.DoesUsernameExist(r.User)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return updatedAt, nil, err
|
||||
return
|
||||
}
|
||||
if dn == "" {
|
||||
return updatedAt, nil, errNoSuchUser
|
||||
err = errNoSuchUser
|
||||
return
|
||||
}
|
||||
isGroup = false
|
||||
} else {
|
||||
if exists, err := sys.LDAPConfig.DoesGroupDNExist(r.Group); err != nil {
|
||||
var exists bool
|
||||
if exists, err = sys.LDAPConfig.DoesGroupDNExist(r.Group); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return updatedAt, nil, err
|
||||
return
|
||||
} else if !exists {
|
||||
return updatedAt, nil, errNoSuchGroup
|
||||
err = errNoSuchGroup
|
||||
return
|
||||
}
|
||||
dn = r.Group
|
||||
isGroup = true
|
||||
}
|
||||
|
||||
userType := stsUser
|
||||
updatedAt, addedOrRemoved, err = sys.store.PolicyDBUpdate(ctx, dn, isGroup,
|
||||
updatedAt, addedOrRemoved, effectivePolicies, err = sys.store.PolicyDBUpdate(ctx, dn, isGroup,
|
||||
userType, r.Policies, isAttach)
|
||||
if err != nil {
|
||||
return updatedAt, nil, err
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other MinIO peers to reload policy
|
||||
@@ -1623,7 +1705,18 @@ func (sys *IAMSys) PolicyDBUpdateLDAP(ctx context.Context, isAttach bool,
|
||||
}
|
||||
}
|
||||
|
||||
return updatedAt, addedOrRemoved, nil
|
||||
logger.LogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemPolicyMapping,
|
||||
PolicyMapping: &madmin.SRPolicyMapping{
|
||||
UserOrGroup: dn,
|
||||
UserType: int(userType),
|
||||
IsGroup: isGroup,
|
||||
Policy: strings.Join(effectivePolicies, ","),
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PolicyDBGet - gets policy set on a user or group. If a list of groups is
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/kes-go"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
|
||||
+3
-14
@@ -22,7 +22,7 @@ package cmd
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -90,16 +90,6 @@ func (a *AccElem) add(dur time.Duration) {
|
||||
a.N++
|
||||
}
|
||||
|
||||
// Add a duration to a single element.
|
||||
func (a *AccElem) addSize(dur time.Duration, sz int64) {
|
||||
if dur < 0 {
|
||||
dur = 0
|
||||
}
|
||||
a.Total += int64(dur)
|
||||
a.Size += sz
|
||||
a.N++
|
||||
}
|
||||
|
||||
// Merge b into a.
|
||||
func (a *AccElem) merge(b AccElem) {
|
||||
a.N += b.N
|
||||
@@ -156,11 +146,10 @@ func (l *lastMinuteLatency) add(t time.Duration) {
|
||||
}
|
||||
|
||||
// Add a new duration data
|
||||
func (l *lastMinuteLatency) addSize(t time.Duration, sz int64) {
|
||||
sec := time.Now().Unix()
|
||||
func (l *lastMinuteLatency) addAll(sec int64, a AccElem) {
|
||||
l.forwardTo(sec)
|
||||
winIdx := sec % 60
|
||||
l.Totals[winIdx].addSize(t, sz)
|
||||
l.Totals[winIdx].merge(a)
|
||||
l.LastSec = sec
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
||||
return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if bucketName != "" {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (l *lockRESTServer) IsValid(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
func getLockArgs(r *http.Request) (args dsync.LockArgs, err error) {
|
||||
dec := msgpNewReader(io.LimitReader(r.Body, 1000*humanize.KiByte))
|
||||
defer readMsgpReaderPool.Put(dec)
|
||||
defer readMsgpReaderPoolPut(dec)
|
||||
err = args.DecodeMsg(dec)
|
||||
return args, err
|
||||
}
|
||||
|
||||
@@ -255,12 +255,13 @@ type metacacheReader struct {
|
||||
func newMetacacheReader(r io.Reader) *metacacheReader {
|
||||
dec := s2DecPool.Get().(*s2.Reader)
|
||||
dec.Reset(r)
|
||||
mr := msgp.NewReader(dec)
|
||||
mr := msgpNewReader(dec)
|
||||
return &metacacheReader{
|
||||
mr: mr,
|
||||
closer: func() {
|
||||
dec.Reset(nil)
|
||||
s2DecPool.Put(dec)
|
||||
readMsgpReaderPoolPut(mr)
|
||||
},
|
||||
creator: func() error {
|
||||
v, err := mr.ReadByte()
|
||||
|
||||
@@ -149,7 +149,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
if err != nil {
|
||||
// Folder could have gone away in-between
|
||||
if err != errVolumeNotFound && err != errFileNotFound {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "metacache-walk-scan-dir")
|
||||
}
|
||||
if opts.ReportNotFound && err == errFileNotFound && current == opts.BaseDir {
|
||||
return errFileNotFound
|
||||
@@ -211,7 +211,7 @@ func (s *xlStorage) WalkDir(ctx context.Context, opts WalkDirOptions, wr io.Writ
|
||||
// while being concurrently listed at the same time in
|
||||
// such scenarios the 'xl.meta' might get truncated
|
||||
if !IsErrIgnored(err, io.EOF, io.ErrUnexpectedEOF) {
|
||||
logger.LogIf(ctx, err)
|
||||
logger.LogOnceIf(ctx, err, "metacache-walk-read-metadata")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user