mirror of
https://github.com/pgsty/minio.git
synced 2026-08-14 02:33:16 +03:00
Compare commits
222 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2155e74951 | |||
| 4714958e99 | |||
| c6e62b9175 | |||
| ab66b23194 | |||
| 73f9d8a636 | |||
| 541a778d7b | |||
| d49f2ec19c | |||
| 8dd63a462f | |||
| 9902c9baaa | |||
| 7de29e6e6b | |||
| 336460f67e | |||
| 95e89f1712 | |||
| 886ae15464 | |||
| d8af244708 | |||
| c8243706b4 | |||
| 30707659b5 | |||
| 90c365a174 | |||
| 7b732b566f | |||
| ba52a925f9 | |||
| fdda5f98c6 | |||
| fa4d627b57 | |||
| 2c3e34f001 | |||
| 7f8f1ad4e3 | |||
| 6f992134a2 | |||
| 0c80bf45d0 | |||
| 2777956581 | |||
| b207520d98 | |||
| 2196fd9cd5 | |||
| ef6304c5c2 | |||
| 6b984410d5 | |||
| 813e0fc1a8 | |||
| 38cf263409 | |||
| 6f6a2214fc | |||
| 791821d590 | |||
| 9a951da881 | |||
| e7a0be5bd3 | |||
| ff932ca2a0 | |||
| 818d3bcaf5 | |||
| 45b1c66195 | |||
| da04cb91ce | |||
| cfc9cfd84a | |||
| ea18e51f4d | |||
| 3d3beb6a9d | |||
| 27b8f18cce | |||
| bf545dc320 | |||
| f001e99fcd | |||
| b4bfdc92cc | |||
| ae654831aa | |||
| 1ffa983a9d | |||
| ecf1566266 | |||
| c5b87f93dd | |||
| b1a2169dcc | |||
| d45a1808f2 | |||
| db2155551a | |||
| 09d35d3b4c | |||
| 5b9342d35c | |||
| 8d98662633 | |||
| 7fdeb44372 | |||
| 59dced8237 | |||
| 496f4a7dc7 | |||
| 8b880a246a | |||
| eeb5942b6b | |||
| 7ec904d67b | |||
| c9212819af | |||
| 10fd53d6bb | |||
| 3fea1d5e35 | |||
| 35ecc04223 | |||
| 3ca9f5ffa3 | |||
| 2e9fed1a14 | |||
| 6b92f3fd99 | |||
| 06e30b5aa1 | |||
| 603cf2a8bb | |||
| a54cdb9587 | |||
| ed4bd20a7c | |||
| cfd12914e1 | |||
| c55aeaf814 | |||
| fdf65aa9b9 | |||
| 69b2aacf5a | |||
| 0af62d35a0 | |||
| 7c32f3f554 | |||
| cec8cdb35e | |||
| 5ab9cc029d | |||
| 667f42515a | |||
| 3614cb7a8b | |||
| b809c84338 | |||
| 33edb072a3 | |||
| 6a00eb10bf | |||
| 792ee48d2c | |||
| 52873ac3a3 | |||
| 88ae0f1196 | |||
| 23a0415eb7 | |||
| 75a0661213 | |||
| a1c7c9ea73 | |||
| 7f19a9a617 | |||
| 2f2c7d91a8 | |||
| 9ad1c2d07d | |||
| 07a7f329e7 | |||
| c7ca791c58 | |||
| d9be8bc693 | |||
| 9fc7537f2a | |||
| f1b2462193 | |||
| 8fbf2b0b2a | |||
| c93157019f | |||
| e3b44c3829 | |||
| 978bd4e2c4 | |||
| bb942c7311 | |||
| 5d25b10f72 | |||
| eac02c04f7 | |||
| 1330e59307 | |||
| 2dd14c0b89 | |||
| 5b8975bf4b | |||
| 6f66f1a910 | |||
| deb3911f5e | |||
| 23a8411732 | |||
| ece0d4ac53 | |||
| 4c92bec619 | |||
| dcd63b4146 | |||
| 224b4f13b8 | |||
| 51a9d1bdb7 | |||
| b2db1e96e2 | |||
| ab7d3cd508 | |||
| 852fb320f7 | |||
| 8fb37a8417 | |||
| d4dcf1d722 | |||
| 02a59a04d1 | |||
| 16a6e68d7b | |||
| 1b427ddb69 | |||
| 02acff7fac | |||
| 712e82344c | |||
| 9f298d2311 | |||
| 716a52f261 | |||
| e4020fb41f | |||
| d1144c2c7e | |||
| 78125ee853 | |||
| 9ecd66007f | |||
| 64ec17b463 | |||
| 6b9805e891 | |||
| 013773065c | |||
| 7d6766adc6 | |||
| c56c2f5fd3 | |||
| 7e819d00ea | |||
| 33767266e7 | |||
| 63be4709b7 | |||
| 6b1f2fc133 | |||
| f749a9bf0e | |||
| 9b4d46a6ed | |||
| 502e652b7a | |||
| de924605a1 | |||
| 15e2ea2c96 | |||
| 07edb7c7f8 | |||
| d0cea7adea | |||
| 2165d45d3f | |||
| 6d5d77f62c | |||
| 49df290270 | |||
| b1bfd75fcf | |||
| e5951e30d0 | |||
| 45d725c0a3 | |||
| c2c5b09bb1 | |||
| 086fbb745e | |||
| 4f37c8ccf2 | |||
| 026265f8f7 | |||
| f91c072f61 | |||
| 52bdbcd046 | |||
| 301c50b721 | |||
| e9c111c8d0 | |||
| 278a165674 | |||
| a05af9bb98 | |||
| e934c3e2a2 | |||
| 2d295a31de | |||
| 9bbf5cb74f | |||
| 680e493065 | |||
| 1ea2449269 | |||
| 7ce63b3078 | |||
| 7432b5c9b2 | |||
| d76160c245 | |||
| 5d838edcef | |||
| c9116e6bd7 | |||
| d7dc9aaf52 | |||
| bfe8a9bccc | |||
| 9990464cd5 | |||
| 881e983ed9 | |||
| f6a7d4d29b | |||
| f98616dce7 | |||
| 5bd0e95eef | |||
| 0414f01b6e | |||
| 0cbebf0f57 | |||
| 4cb6ebcfa2 | |||
| 2232e095d5 | |||
| aae337f5b5 | |||
| a78e5d4763 | |||
| cf37c7997e | |||
| 1ffbb5c24c | |||
| b9c48e0ab0 | |||
| fe5d599802 | |||
| 55063906b5 | |||
| c7178d2066 | |||
| f14f60a487 | |||
| e2b3c083aa | |||
| 3011692d93 | |||
| 86252ec7e1 | |||
| ef1aa870c5 | |||
| c260182412 | |||
| 61c17c8933 | |||
| dd93eee1e3 | |||
| 7a400542ae | |||
| 88286cf8d0 | |||
| 8cb6184f1d | |||
| b849fd7a75 | |||
| 09ee145e9c | |||
| 04dfcbfdd7 | |||
| 23e46f9dba | |||
| fc5213258e | |||
| 005ebbb9b2 | |||
| 017067e11f | |||
| 2bb69033e5 | |||
| fca4ee84c9 | |||
| 60e60f68dd | |||
| ba758361b3 | |||
| c6b218e5df | |||
| c18fbdb29a | |||
| b1ad99edbf | |||
| 080e0c2323 |
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"numFilesToCheck": 10,
|
||||
"requiredOrgs": ["minio"]
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# MinIO Contribution Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
# MinIO Contribution Guide [](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
``MinIO`` community welcomes your contribution. To make the process as seamless as possible, we recommend you read this contribution guide.
|
||||
|
||||
|
||||
@@ -11366,28 +11366,28 @@ SOFTWARE.
|
||||
github.com/klauspost/readahead
|
||||
https://github.com/klauspost/readahead
|
||||
----------------------------------------------------------------
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Klaus Post
|
||||
|
||||
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.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Klaus Post
|
||||
|
||||
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.
|
||||
|
||||
|
||||
================================================================
|
||||
|
||||
@@ -12375,6 +12375,214 @@ https://github.com/minio/minio-go/v6
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/minio-go/v6
|
||||
https://github.com/minio/minio-go/v6
|
||||
----------------------------------------------------------------
|
||||
|
||||
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/minio/parquet-go
|
||||
https://github.com/minio/parquet-go
|
||||
----------------------------------------------------------------
|
||||
@@ -16208,7 +16416,7 @@ github.com/pascaldekloe/goe
|
||||
https://github.com/pascaldekloe/goe
|
||||
----------------------------------------------------------------
|
||||
To the extent possible under law, Pascal S. de Kloe has waived all
|
||||
copyright and related or neighboring rights to Go Enterprise. This
|
||||
copyright and related or neighboring rights to Go Enterprice. This
|
||||
work is published from The Netherlands.
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/legalcode
|
||||
@@ -16312,36 +16520,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/pkg/profile
|
||||
https://github.com/pkg/profile
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2013 Dave Cheney. 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.
|
||||
|
||||
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/pmezard/go-difflib
|
||||
https://github.com/pmezard/go-difflib
|
||||
----------------------------------------------------------------
|
||||
|
||||
+1
-1
@@ -5,7 +5,6 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
ENV GOPATH /go
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
ENV GOPROXY https://proxy.golang.org
|
||||
|
||||
RUN \
|
||||
apk add --no-cache git && \
|
||||
@@ -23,6 +22,7 @@ ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
EXPOSE 9000
|
||||
|
||||
COPY --from=0 /go/bin/minio /usr/bin/minio
|
||||
COPY CREDITS /third_party/
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||
|
||||
RUN \
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
FROM golang:1.13-alpine as builder
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
|
||||
RUN \
|
||||
apk add --no-cache git 'curl>7.61.0' && \
|
||||
git clone https://github.com/minio/minio && \
|
||||
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||
|
||||
FROM arm32v7/alpine:3.10
|
||||
|
||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||
COPY CREDITS /third_party/
|
||||
COPY --from=builder /home/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
|
||||
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.min.io/server/minio/release/linux-arm/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -0,0 +1,41 @@
|
||||
FROM golang:1.13-alpine as builder
|
||||
|
||||
WORKDIR /home
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
|
||||
RUN \
|
||||
apk add --no-cache git 'curl>7.61.0' && \
|
||||
git clone https://github.com/minio/minio && \
|
||||
curl -L https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz | tar zxvf - -C . && mv qemu-3.0.0+resin-arm/qemu-arm-static .
|
||||
|
||||
FROM arm64v8/alpine:3.10
|
||||
|
||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||
COPY CREDITS /third_party/
|
||||
COPY --from=builder /home/qemu-arm-static /usr/bin/qemu-arm-static
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_KMS_MASTER_KEY_FILE=kms_master_key \
|
||||
MINIO_SSE_MASTER_KEY_FILE=sse_master_key
|
||||
|
||||
RUN \
|
||||
apk add --no-cache ca-certificates 'curl>7.61.0' 'su-exec>=0.2' && \
|
||||
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf && \
|
||||
curl https://dl.min.io/server/minio/release/linux-arm64/minio > /usr/bin/minio && \
|
||||
chmod +x /usr/bin/minio && \
|
||||
chmod +x /usr/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 9000
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
CMD ["minio"]
|
||||
@@ -4,6 +4,7 @@ LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||
COPY minio /usr/bin/
|
||||
COPY CREDITS /third_party/
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
|
||||
@@ -13,6 +13,7 @@ FROM alpine:3.10
|
||||
LABEL maintainer="MinIO Inc <dev@min.io>"
|
||||
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/
|
||||
COPY CREDITS /third_party/
|
||||
|
||||
ENV MINIO_UPDATE off
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
|
||||
@@ -22,7 +22,7 @@ getdeps:
|
||||
ifeq ($(GOARCH),s390x)
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck)
|
||||
else
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet https://github.com/dominikh/go-tools/releases/download/2019.2.3/staticcheck_${GOOS}_${GOARCH}.tar.gz && tar xf staticcheck_${GOOS}_${GOARCH}.tar.gz && mv staticcheck/staticcheck ${GOPATH}/bin/staticcheck && chmod +x ${GOPATH}/bin/staticcheck && rm -f staticcheck_${GOOS}_${GOARCH}.tar.gz && rm -rf staticcheck)
|
||||
@which staticcheck 1>/dev/null || (echo "Installing staticcheck" && wget --quiet https://github.com/dominikh/go-tools/releases/download/2020.1.3/staticcheck_${GOOS}_${GOARCH}.tar.gz && tar xf staticcheck_${GOOS}_${GOARCH}.tar.gz && mv staticcheck/staticcheck ${GOPATH}/bin/staticcheck && chmod +x ${GOPATH}/bin/staticcheck && rm -f staticcheck_${GOOS}_${GOARCH}.tar.gz && rm -rf staticcheck)
|
||||
endif
|
||||
@which misspell 1>/dev/null || (echo "Installing misspell" && GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell)
|
||||
|
||||
@@ -71,19 +71,19 @@ test-race: verifiers build
|
||||
# Verify minio binary
|
||||
verify:
|
||||
@echo "Verifying build with race"
|
||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
|
||||
# Verify healing of disks with minio binary
|
||||
verify-healing:
|
||||
@echo "Verify healing build with race"
|
||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||
|
||||
# Builds minio locally.
|
||||
build: checks
|
||||
@echo "Building minio binary to './minio'"
|
||||
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
@GO111MODULE=on CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags $(BUILD_LDFLAGS) -o $(PWD)/minio 1>/dev/null
|
||||
|
||||
docker: build
|
||||
@docker build -t $(TAG) . -f Dockerfile.dev
|
||||
@@ -101,3 +101,4 @@ clean:
|
||||
@rm -rvf minio
|
||||
@rm -rvf build
|
||||
@rm -rvf release
|
||||
@rm -rvf .verify*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
MinIO Cloud Storage, (C) 2014,2015 MinIO, Inc.
|
||||
MinIO Cloud Storage, (C) 2014-2020 MinIO, Inc.
|
||||
|
||||
This product includes software developed at MinIO, Inc.
|
||||
(https://min.io/).
|
||||
@@ -6,4 +6,4 @@ This product includes software developed at MinIO, Inc.
|
||||
The MinIO project contains unmodified/modified subcomponents too with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for these subcomponents is subject to the terms and conditions
|
||||
of the following licenses.
|
||||
of Apache License Version 2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# MinIO Quickstart Guide
|
||||
[](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
[](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
[](https://min.io)
|
||||
|
||||
@@ -21,7 +21,7 @@ docker run -p 9000:9000 minio/minio:edge server /data
|
||||
> NOTE: Docker will not display the default keys unless you start the container with the `-it`(interactive TTY) argument. Generally, it is not recommended to use default keys with containers. Please visit MinIO Docker quickstart guide for more information [here](https://docs.min.io/docs/minio-docker-quickstart-guide)
|
||||
|
||||
## macOS
|
||||
### Homebrew
|
||||
### Homebrew (recommended)
|
||||
Install minio packages using [Homebrew](http://brew.sh/)
|
||||
```sh
|
||||
brew install minio/stable/minio
|
||||
@@ -74,7 +74,7 @@ minio.exe server D:\Photos
|
||||
|
||||
## FreeBSD
|
||||
### Port
|
||||
Install minio packages using [pkg](https://github.com/freebsd/pkg)
|
||||
Install minio packages using [pkg](https://github.com/freebsd/pkg), MinIO doesn't officially build FreeBSD binaries but is maintained by FreeBSD upstream [here](https://www.freshports.org/www/minio).
|
||||
|
||||
```sh
|
||||
pkg install minio
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# MinIO Quickstart Guide [](https://slack.min.io) [](https://goreportcard.com/report/minio/minio) [](https://hub.docker.com/r/minio/minio/)
|
||||
# MinIO Quickstart Guide [](https://slack.min.io) [](https://hub.docker.com/r/minio/minio/)
|
||||
|
||||
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
|
||||
|
||||
|
||||
@@ -148,7 +148,8 @@ export class Login extends React.Component {
|
||||
<OpenIDLoginButton
|
||||
className="btn openid-btn"
|
||||
clientId={this.state.clientId}
|
||||
authorizationEndpoint={this.state.discoveryDoc.authorization_endpoint}
|
||||
authEp={this.state.discoveryDoc.authorization_endpoint}
|
||||
authScopes={this.state.discoveryDoc.scopes_supported}
|
||||
>
|
||||
Log in with OpenID
|
||||
</OpenIDLoginButton>
|
||||
|
||||
@@ -66,6 +66,7 @@ export class OpenIDLogin extends React.Component {
|
||||
|
||||
const authURL = buildOpenIDAuthURL(
|
||||
this.state.discoveryDoc.authorization_endpoint,
|
||||
this.state.discoveryDoc.scopes_supported,
|
||||
redirectURI,
|
||||
this.state.clientID,
|
||||
nonce
|
||||
|
||||
@@ -27,7 +27,7 @@ export class OpenIDLoginButton extends React.Component {
|
||||
|
||||
handleClick(event) {
|
||||
event.stopPropagation()
|
||||
const { authorizationEndpoint, clientId } = this.props
|
||||
const { authEp, authScopes, clientId } = this.props
|
||||
|
||||
let redirectURI = window.location.href.split("#")[0]
|
||||
if (redirectURI.endsWith('/')) {
|
||||
@@ -40,7 +40,7 @@ export class OpenIDLoginButton extends React.Component {
|
||||
const nonce = getRandomString(16)
|
||||
storage.setItem(OPEN_ID_NONCE_KEY, nonce)
|
||||
|
||||
const authURL = buildOpenIDAuthURL(authorizationEndpoint, redirectURI, clientId, nonce)
|
||||
const authURL = buildOpenIDAuthURL(authEp, authScopes, redirectURI, clientId, nonce)
|
||||
window.location = authURL
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
|
||||
export const OPEN_ID_NONCE_KEY = 'openIDKey'
|
||||
|
||||
export const buildOpenIDAuthURL = (authorizationEndpoint, redirectURI, clientID, nonce) => {
|
||||
export const buildOpenIDAuthURL = (authEp, authScopes, redirectURI, clientID, nonce) => {
|
||||
const params = new URLSearchParams()
|
||||
params.set("response_type", "id_token")
|
||||
params.set("scope", "openid")
|
||||
params.set("scope", authScopes.join(" "))
|
||||
params.set("client_id", clientID)
|
||||
params.set("redirect_uri", redirectURI)
|
||||
params.set("nonce", nonce)
|
||||
|
||||
return `${authorizationEndpoint}?${params.toString()}`
|
||||
return `${authEp}?${params.toString()}`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
checks = ["all", "-ST1005", "-ST1000", "-SA4000", "-SA9004", "-SA1019", "-SA1008", "-U1000", "-ST1003", "-ST1018"]
|
||||
+31
-31
File diff suppressed because one or more lines are too long
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# usage: ./benchcmp.sh <commit-sha1> <commit-sha2>
|
||||
# Exit on any non zero return value on execution of a command.
|
||||
set -e
|
||||
|
||||
# path of benchcmp.
|
||||
benchcmp=${GOPATH}/bin/benchcmp
|
||||
|
||||
# function which runs the benchmark comparison.
|
||||
RunBenchCmp () {
|
||||
# Path for storing output of benchmark at commit 1.
|
||||
commit1Bench=/tmp/minio-$1.bench
|
||||
# Path for storing output of benchmark at commit 2.
|
||||
commit2Bench=/tmp/minio-$2.bench
|
||||
# switch to commit $1.
|
||||
git checkout $1
|
||||
# Check if the benchmark results for given commit 1 already exists.
|
||||
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
|
||||
if [[ ! -f $commit1Bench ]]
|
||||
then
|
||||
|
||||
echo "Running benchmarks at $1"
|
||||
go test -run=NONE -bench=. | tee $commit1Bench
|
||||
fi
|
||||
# get back to the commit from which it was started.
|
||||
git checkout -
|
||||
echo "Checking into commit $2"
|
||||
# switch to commit $2
|
||||
git checkout $2
|
||||
# Check if the benchmark results for given commit 2 already exists.
|
||||
# Benchmarks are time/resource consuming operations, run only if the the results doesn't exist.
|
||||
if [[ ! -f $commit2Bench ]]
|
||||
then
|
||||
# Running benchmarks at $2.
|
||||
echo "Running benchmarks at $2"
|
||||
go test -run=NONE -bench=. | tee $commit2Bench
|
||||
fi
|
||||
|
||||
# get back to the commit from which it was started.
|
||||
git checkout -
|
||||
# Comparing the benchmarks.
|
||||
echo "Running benchmark comparison between $1 and $2 ..."
|
||||
$benchcmp $commit1Bench $commit2Bench
|
||||
echo "Done."
|
||||
}
|
||||
|
||||
# check if 2 commit SHA's of snapshots of code for which benchmp has to be done is provided.
|
||||
if [ ! $# -eq 2 ]
|
||||
then
|
||||
# exit if commit SHA's are not provided.
|
||||
echo $#
|
||||
echo "Need Commit SHA's of 2 snapshots to be supplied to run benchmark comparison."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if benchcmp exists.
|
||||
if [[ -x "$benchcmp" ]]
|
||||
then
|
||||
RunBenchCmp $1 $2
|
||||
else
|
||||
# install benchcmp if doesnt't exist.
|
||||
echo "fetching Benchcmp..."
|
||||
go get -u golang.org/x/tools/cmd/benchcmp
|
||||
echo "Done."
|
||||
RunBenchCmp $1 $2
|
||||
fi
|
||||
@@ -1,14 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
# Enable tracing if set.
|
||||
[ -n "$BASH_XTRACEFD" ] && set -ex
|
||||
[ -n "$BASH_XTRACEFD" ] && set -x
|
||||
|
||||
function _init() {
|
||||
## All binaries are static make sure to disable CGO.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
## List of architectures and OS to test coss compilation.
|
||||
SUPPORTED_OSARCH="linux/ppc64le linux/arm64 linux/s390x darwin/amd64 freebsd/amd64"
|
||||
SUPPORTED_OSARCH="linux/ppc64le linux/arm64 linux/s390x darwin/amd64 freebsd/amd64 windows/amd64 linux/arm linux/386"
|
||||
}
|
||||
|
||||
function _build() {
|
||||
@@ -19,11 +20,11 @@ function _build() {
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
printf -- "--> %15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# Go build to build the binary.
|
||||
# go build -trimpath to build the binary.
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
export GO111MODULE=on
|
||||
go build -tags kqueue -o /dev/null
|
||||
go build -trimpath -tags kqueue -o /dev/null
|
||||
}
|
||||
|
||||
function main() {
|
||||
|
||||
+38
-109
@@ -26,37 +26,53 @@ fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" )
|
||||
MINIO=( "$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server )
|
||||
|
||||
function start_minio_3_node() {
|
||||
declare -a minio_pids
|
||||
declare -a ARGS
|
||||
export MINIO_ACCESS_KEY=minio
|
||||
export MINIO_SECRET_KEY=minio123
|
||||
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
for i in $(seq 1 3); do
|
||||
ARGS+=("http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[9000+$i]${WORK_DIR}/$i/6/")
|
||||
ARGS+=("http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/1/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/2/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/3/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/4/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/5/ http://127.0.0.1:$[$start_port+$i]${WORK_DIR}/$i/6/")
|
||||
done
|
||||
|
||||
"${MINIO[@]}" server --address ":9001" ${ARGS[@]} > "${WORK_DIR}/dist-minio-9001.log" 2>&1 &
|
||||
"${MINIO[@]}" --address ":$[$start_port+1]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server1.log" 2>&1 &
|
||||
minio_pids[0]=$!
|
||||
disown "${minio_pids[0]}"
|
||||
|
||||
"${MINIO[@]}" server --address ":9002" ${ARGS[@]} > "${WORK_DIR}/dist-minio-9002.log" 2>&1 &
|
||||
"${MINIO[@]}" --address ":$[$start_port+2]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server2.log" 2>&1 &
|
||||
minio_pids[1]=$!
|
||||
disown "${minio_pids[1]}"
|
||||
|
||||
"${MINIO[@]}" server --address ":9003" ${ARGS[@]} > "${WORK_DIR}/dist-minio-9003.log" 2>&1 &
|
||||
"${MINIO[@]}" --address ":$[$start_port+3]" ${ARGS[@]} > "${WORK_DIR}/dist-minio-server3.log" 2>&1 &
|
||||
minio_pids[2]=$!
|
||||
disown "${minio_pids[2]}"
|
||||
|
||||
sleep "$1"
|
||||
echo "${minio_pids[@]}"
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
if ! kill "$pid"; then
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||
done
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
# forcibly killing, to proceed further properly.
|
||||
kill -9 "$pid"
|
||||
sleep 1 # wait 1sec per pid
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function check_online() {
|
||||
for i in $(seq 1 3); do
|
||||
if grep -q 'Server switching to safe mode' ${WORK_DIR}/dist-minio-$[9000+$i].log; then
|
||||
echo "1"
|
||||
fi
|
||||
done
|
||||
if grep -q 'Server switching to safe mode' ${WORK_DIR}/dist-minio-*.log; then
|
||||
echo "1"
|
||||
fi
|
||||
}
|
||||
|
||||
function purge()
|
||||
@@ -74,109 +90,22 @@ function __init__()
|
||||
echo '{"version": "3", "credential": {"accessKey": "minio", "secretKey": "minio123"}, "region": "us-east-1"}' > "$MINIO_CONFIG_DIR/config.json"
|
||||
}
|
||||
|
||||
function perform_test_1() {
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
function perform_test() {
|
||||
start_minio_3_node 60
|
||||
|
||||
echo "Testing in Distributed Erasure setup healing test case 1"
|
||||
echo "Remove the contents of the disks belonging to '2' erasure set"
|
||||
echo "Testing Distributed Erasure setup healing of drives"
|
||||
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
||||
|
||||
rm -rf ${WORK_DIR}/2/*
|
||||
rm -rf ${WORK_DIR}/${1}/*/
|
||||
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
if ! kill "$pid"; then
|
||||
cat "${WORK_DIR}/*.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
start_minio_3_node 60
|
||||
|
||||
rv=$(check_online)
|
||||
if [ "$rv" == "1" ]; then
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill -9 "$pid"
|
||||
done
|
||||
pkill -9 minio
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-minio-$[9000+$i].log"
|
||||
done
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function perform_test_2() {
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
|
||||
echo "Testing in Distributed Erasure setup healing test case 2"
|
||||
echo "Remove the contents of the disks belonging to '1' erasure set"
|
||||
|
||||
rm -rf ${WORK_DIR}/1/*/
|
||||
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
if ! kill "$pid"; then
|
||||
cat "${WORK_DIR}/*.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
rv=$(check_online)
|
||||
|
||||
if [ "$rv" == "1" ]; then
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill -9 "$pid"
|
||||
done
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-minio-$[9000+$i].log"
|
||||
done
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function perform_test_3() {
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
|
||||
echo "Testing in Distributed Erasure setup healing test case 3"
|
||||
echo "Remove the contents of the disks belonging to '3' erasure set"
|
||||
|
||||
rm -rf ${WORK_DIR}/3/*/
|
||||
|
||||
minio_pids=( $(start_minio_3_node 60) )
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
if ! kill "$pid"; then
|
||||
cat "${WORK_DIR}/*.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
rv=$(check_online)
|
||||
|
||||
if [ "$rv" == "1" ]; then
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill -9 "$pid"
|
||||
done
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-minio-$[9000+$i].log"
|
||||
cat "${WORK_DIR}/dist-minio-server$i.log"
|
||||
done
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
@@ -186,9 +115,9 @@ function perform_test_3() {
|
||||
|
||||
function main()
|
||||
{
|
||||
perform_test_1
|
||||
perform_test_2
|
||||
perform_test_3
|
||||
perform_test "2"
|
||||
perform_test "1"
|
||||
perform_test "3"
|
||||
}
|
||||
|
||||
( __init__ "$@" && main "$@" )
|
||||
|
||||
+141
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2018-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,11 +18,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
// Data types used for returning dummy access control
|
||||
@@ -50,6 +53,71 @@ type accessControlPolicy struct {
|
||||
} `xml:"AccessControlList"`
|
||||
}
|
||||
|
||||
// PutBucketACLHandler - PUT Bucket ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL subresource
|
||||
// to set ACL for a bucket, this is a dummy call
|
||||
// only responds success if the ACL is private.
|
||||
func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketACL")
|
||||
|
||||
defer logger.AuditLog(w, r, "PutBucketACL", mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Allow putBucketACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
aclHeader := r.Header.Get(xhttp.AmzACL)
|
||||
if aclHeader == "" {
|
||||
acl := &accessControlPolicy{}
|
||||
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||
if err == io.EOF {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingSecurityHeader),
|
||||
r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if len(acl.AccessControlList.Grants) == 0 {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if aclHeader != "" && aclHeader != "private" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// GetBucketACLHandler - GET Bucket ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL
|
||||
@@ -99,6 +167,71 @@ func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// PutObjectACLHandler - PUT Object ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL subresource
|
||||
// to set ACL for a bucket, this is a dummy call
|
||||
// only responds success if the ACL is private.
|
||||
func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutObjectACL")
|
||||
|
||||
defer logger.AuditLog(w, r, "PutObjectACL", mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object, err := url.PathUnescape(vars["object"])
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Allow putObjectACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if object exists.
|
||||
_, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
aclHeader := r.Header.Get(xhttp.AmzACL)
|
||||
if aclHeader == "" {
|
||||
acl := &accessControlPolicy{}
|
||||
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if len(acl.AccessControlList.Grants) == 0 {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if aclHeader != "" && aclHeader != "private" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, errInvalidArgument), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// GetObjectACLHandler - GET Object ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL
|
||||
@@ -110,7 +243,11 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
object, err := url.PathUnescape(vars["object"])
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
@@ -126,7 +263,7 @@ func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
// Before proceeding validate if object exists.
|
||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
_, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
|
||||
+118
-2
@@ -97,11 +97,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListUsers")
|
||||
|
||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
|
||||
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.ListUsersAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
|
||||
allCredentials, err := globalIAMSys.ListUsers()
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -114,7 +116,6 @@ func (a adminAPIHandlers) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
password := globalActiveCred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -378,6 +379,121 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddServiceAccount - PUT /minio/admin/v2/add-service-account?parentUser=<parent_user_accesskey>
|
||||
func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AddServiceAccount")
|
||||
|
||||
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.CreateUserAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var createReq madmin.AddServiceAccountReq
|
||||
if err = json.Unmarshal(configBytes, &createReq); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if createReq.Parent == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Disallow creating service accounts for the root user
|
||||
if createReq.Parent == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
creds, err := globalIAMSys.NewServiceAccount(ctx, createReq.Parent, createReq.Policy)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Notify all other Minio peers to reload user
|
||||
for _, nerr := range globalNotificationSys.LoadUser(creds.AccessKey, false) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
}
|
||||
}
|
||||
|
||||
var createResp = madmin.AddServiceAccountResp{
|
||||
Credentials: creds,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(createResp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// GetServiceAccount - GET /minio/admin/v2/get-service-account?accessKey=<access_key>
|
||||
func (a adminAPIHandlers) GetServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetServiceAccount")
|
||||
|
||||
objectAPI, cred := validateAdminUsersReq(ctx, w, r, iampolicy.GetUserAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
|
||||
password := cred.SecretKey
|
||||
|
||||
creds, err := globalIAMSys.GetServiceAccount(ctx, accessKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(creds)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// InfoCannedPolicy - GET /minio/admin/v2/info-canned-policy?name={policyName}
|
||||
func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "InfoCannedPolicy")
|
||||
|
||||
+259
-66
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -41,6 +42,7 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
"github.com/minio/minio/pkg/event/target"
|
||||
@@ -140,9 +142,9 @@ func (a adminAPIHandlers) ServerUpdateHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
u.Path = path.Dir(u.Path) + "minio.exe"
|
||||
u.Path = path.Dir(u.Path) + SlashSeparator + "minio.exe"
|
||||
} else {
|
||||
u.Path = path.Dir(u.Path) + "minio"
|
||||
u.Path = path.Dir(u.Path) + SlashSeparator + "minio"
|
||||
}
|
||||
|
||||
updateURL = u.String()
|
||||
@@ -270,12 +272,12 @@ type ServerInfo struct {
|
||||
// Get server information
|
||||
func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "StorageInfo")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.StorageInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
storageInfo := objectAPI.StorageInfo(ctx)
|
||||
storageInfo := objectAPI.StorageInfo(ctx, false)
|
||||
|
||||
// Marshal API response
|
||||
jsonBytes, err := json.Marshal(storageInfo)
|
||||
@@ -295,26 +297,87 @@ func (a adminAPIHandlers) StorageInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
// Get server/cluster data usage info
|
||||
func (a adminAPIHandlers) DataUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DataUsageInfo")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DataUsageInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
dataUsageInfoJSON, err := json.Marshal(dataUsageInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, dataUsageInfoJSON)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) AccountingUsageInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "AccountingUsageInfo")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.AccountingUsageInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var accountingUsageInfo = make(map[string]madmin.BucketAccountingUsage)
|
||||
|
||||
buckets, err := objectAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
// writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := globalIAMSys.ListUsers()
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Load the latest calculated data usage
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
// Calculate for each bucket, which users are allowed to access to it
|
||||
for _, bucket := range buckets {
|
||||
bucketUsageInfo := madmin.BucketAccountingUsage{}
|
||||
|
||||
// Fetch the data usage of the current bucket
|
||||
if !dataUsageInfo.LastUpdate.IsZero() && dataUsageInfo.BucketsSizes != nil {
|
||||
bucketUsageInfo.Size = dataUsageInfo.BucketsSizes[bucket.Name]
|
||||
}
|
||||
|
||||
for user := range users {
|
||||
rd, wr, custom := globalIAMSys.GetAccountAccess(user, bucket.Name)
|
||||
if rd || wr || custom {
|
||||
bucketUsageInfo.AccessList = append(bucketUsageInfo.AccessList, madmin.AccountAccess{
|
||||
AccountName: user,
|
||||
Read: rd,
|
||||
Write: wr,
|
||||
Custom: custom,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
accountingUsageInfo[bucket.Name] = bucketUsageInfo
|
||||
}
|
||||
|
||||
usageInfoJSON, err := json.Marshal(accountingUsageInfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, usageInfoJSON)
|
||||
}
|
||||
|
||||
// ServerCPULoadInfo holds informantion about cpu utilization
|
||||
// of one minio node. It also reports any errors if encountered
|
||||
// while trying to reach this server.
|
||||
@@ -349,7 +412,7 @@ type ServerNetReadPerfInfo struct {
|
||||
func (a adminAPIHandlers) PerfInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PerfInfo")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.PerfInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -520,7 +583,7 @@ type PeerLocks struct {
|
||||
func (a adminAPIHandlers) TopLocksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "TopLocks")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.TopLocksAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -565,7 +628,7 @@ type StartProfilingResult struct {
|
||||
func (a adminAPIHandlers) StartProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "StartProfiling")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -661,7 +724,7 @@ func (f dummyFileInfo) Sys() interface{} { return f.sys }
|
||||
func (a adminAPIHandlers) DownloadProfilingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DownloadProfiling")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ProfilingAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -835,7 +898,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// find number of disks in the setup
|
||||
info := objectAPI.StorageInfo(ctx)
|
||||
info := objectAPI.StorageInfo(ctx, false)
|
||||
numDisks := info.Backend.OfflineDisks.Sum() + info.Backend.OnlineDisks.Sum()
|
||||
|
||||
healPath := pathJoin(hip.bucket, hip.objPrefix)
|
||||
@@ -920,13 +983,24 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
// Aggregate healing result
|
||||
var aggregatedHealStateResult = madmin.BgHealState{}
|
||||
var aggregatedHealStateResult = madmin.BgHealState{
|
||||
ScannedItemsCount: bgHealStates[0].ScannedItemsCount,
|
||||
LastHealActivity: bgHealStates[0].LastHealActivity,
|
||||
NextHealRound: bgHealStates[0].NextHealRound,
|
||||
}
|
||||
|
||||
bgHealStates = bgHealStates[1:]
|
||||
|
||||
for _, state := range bgHealStates {
|
||||
aggregatedHealStateResult.ScannedItemsCount += state.ScannedItemsCount
|
||||
if aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
||||
if !state.LastHealActivity.IsZero() && aggregatedHealStateResult.LastHealActivity.Before(state.LastHealActivity) {
|
||||
aggregatedHealStateResult.LastHealActivity = state.LastHealActivity
|
||||
// The node which has the last heal activity means its
|
||||
// is the node that is orchestrating self healing operations,
|
||||
// which also means it is the same node which decides when
|
||||
// the next self healing operation will be done.
|
||||
aggregatedHealStateResult.NextHealRound = state.NextHealRound
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(aggregatedHealStateResult); err != nil {
|
||||
@@ -1051,7 +1125,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
trcErr := r.URL.Query().Get("err") == "true"
|
||||
|
||||
// Validate request signature.
|
||||
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.ListServerInfoAdminAction, "")
|
||||
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.TraceAdminAction, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||
return
|
||||
@@ -1105,7 +1179,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ConsoleLog")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ConsoleLogAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -1154,8 +1228,8 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
for {
|
||||
select {
|
||||
case entry := <-logCh:
|
||||
log := entry.(madmin.LogInfo)
|
||||
if log.SendLog(node, logKind) {
|
||||
log, ok := entry.(log.Info)
|
||||
if ok && log.SendLog(node, logKind) {
|
||||
if err := enc.Encode(log); err != nil {
|
||||
return
|
||||
}
|
||||
@@ -1176,7 +1250,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "KMSKeyStatusHandler")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.KMSKeyStatusAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -1208,20 +1282,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Check whether we can update / re-wrap the sealed key.
|
||||
sealedKey, err = GlobalKMS.UpdateKey(keyID, sealedKey, kmsContext)
|
||||
if err != nil {
|
||||
response.UpdateErr = err.Error()
|
||||
resp, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Verify that we can indeed decrypt the (encrypted) key
|
||||
// 2. Verify that we can indeed decrypt the (encrypted) key
|
||||
decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
|
||||
if err != nil {
|
||||
response.DecryptionErr = err.Error()
|
||||
@@ -1234,7 +1295,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Compare generated key with decrypted key
|
||||
// 3. Compare generated key with decrypted key
|
||||
if subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1 {
|
||||
response.DecryptionErr = "The generated and the decrypted data key do not match"
|
||||
resp, err := json.Marshal(response)
|
||||
@@ -1261,7 +1322,7 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
||||
func (a adminAPIHandlers) ServerHardwareInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "HardwareInfo")
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ListServerInfoAdminAction)
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerHardwareInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -1311,12 +1372,141 @@ func (a adminAPIHandlers) ServerHardwareInfoHandler(w http.ResponseWriter, r *ht
|
||||
}
|
||||
}
|
||||
|
||||
// OBDInfoHandler - GET /minio/admin/v2/obdinfo
|
||||
// ----------
|
||||
// Get server on-board diagnostics
|
||||
func (a adminAPIHandlers) OBDInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "OBDInfo")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.OBDInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
deadlinedCtx, cancel := context.WithDeadline(ctx, time.Now().Add(10*time.Minute))
|
||||
obdInfo := madmin.OBDInfo{}
|
||||
|
||||
setCommonHeaders(w)
|
||||
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
partialWrite := func() {
|
||||
logger.LogIf(ctx, enc.Encode(obdInfo))
|
||||
}
|
||||
|
||||
finish := func() {
|
||||
partialWrite()
|
||||
w.(http.Flusher).Flush()
|
||||
cancel()
|
||||
}
|
||||
|
||||
errResp := func(err error) {
|
||||
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
|
||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||
encodedErrorResponse := encodeResponse(errorResponse)
|
||||
obdInfo.Error = string(encodedErrorResponse)
|
||||
finish()
|
||||
}
|
||||
|
||||
nsLock := objectAPI.NewNSLock(deadlinedCtx, minioMetaBucket, "obd-in-progress")
|
||||
if err := nsLock.GetLock(newDynamicTimeout(10*time.Minute, 600*time.Second)); err != nil { // returns a locked lock
|
||||
errResp(err)
|
||||
return
|
||||
}
|
||||
defer nsLock.Unlock()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
if cpu, ok := vars["syscpu"]; ok && cpu == "true" {
|
||||
cpuInfo := getLocalCPUOBDInfo(deadlinedCtx)
|
||||
|
||||
obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, cpuInfo)
|
||||
obdInfo.Sys.CPUInfo = append(obdInfo.Sys.CPUInfo, globalNotificationSys.CPUOBDInfo(deadlinedCtx)...)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if diskHw, ok := vars["sysdiskhw"]; ok && diskHw == "true" {
|
||||
diskHwInfo := getLocalDiskHwOBD(deadlinedCtx)
|
||||
|
||||
obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, diskHwInfo)
|
||||
obdInfo.Sys.DiskHwInfo = append(obdInfo.Sys.DiskHwInfo, globalNotificationSys.DiskHwOBDInfo(deadlinedCtx)...)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if osInfo, ok := vars["sysosinfo"]; ok && osInfo == "true" {
|
||||
osInfo := getLocalOsInfoOBD(deadlinedCtx)
|
||||
|
||||
obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, osInfo)
|
||||
obdInfo.Sys.OsInfo = append(obdInfo.Sys.OsInfo, globalNotificationSys.OsOBDInfo(deadlinedCtx)...)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if mem, ok := vars["sysmem"]; ok && mem == "true" {
|
||||
memInfo := getLocalMemOBD(deadlinedCtx)
|
||||
|
||||
obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, memInfo)
|
||||
obdInfo.Sys.MemInfo = append(obdInfo.Sys.MemInfo, globalNotificationSys.MemOBDInfo(deadlinedCtx)...)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if proc, ok := vars["sysprocess"]; ok && proc == "true" {
|
||||
procInfo := getLocalProcOBD(deadlinedCtx)
|
||||
|
||||
obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, procInfo)
|
||||
obdInfo.Sys.ProcInfo = append(obdInfo.Sys.ProcInfo, globalNotificationSys.ProcOBDInfo(deadlinedCtx)...)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if config, ok := vars["minioconfig"]; ok && config == "true" {
|
||||
cfg, err := readServerConfig(ctx, objectAPI)
|
||||
logger.LogIf(ctx, err)
|
||||
obdInfo.Minio.Config = cfg
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if drive, ok := vars["perfdrive"]; ok && drive == "true" {
|
||||
// Get drive obd details from local server's drive(s)
|
||||
driveOBDSerial := getLocalDrivesOBD(deadlinedCtx, false, globalEndpoints, r)
|
||||
driveOBDParallel := getLocalDrivesOBD(deadlinedCtx, true, globalEndpoints, r)
|
||||
|
||||
errStr := ""
|
||||
if driveOBDSerial.Error != "" {
|
||||
errStr = "serial: " + driveOBDSerial.Error
|
||||
}
|
||||
if driveOBDParallel.Error != "" {
|
||||
errStr = errStr + " parallel: " + driveOBDParallel.Error
|
||||
}
|
||||
|
||||
driveOBD := madmin.ServerDrivesOBDInfo{
|
||||
Addr: driveOBDSerial.Addr,
|
||||
Serial: driveOBDSerial.Serial,
|
||||
Parallel: driveOBDParallel.Parallel,
|
||||
Error: errStr,
|
||||
}
|
||||
obdInfo.Perf.DriveInfo = append(obdInfo.Perf.DriveInfo, driveOBD)
|
||||
|
||||
// Notify all other MinIO peers to report drive obd numbers
|
||||
driveOBDs := globalNotificationSys.DriveOBDInfo(deadlinedCtx)
|
||||
obdInfo.Perf.DriveInfo = append(obdInfo.Perf.DriveInfo, driveOBDs...)
|
||||
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
if net, ok := vars["perfnet"]; ok && net == "true" && globalIsDistXL {
|
||||
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.NetOBDInfo(deadlinedCtx))
|
||||
obdInfo.Perf.Net = append(obdInfo.Perf.Net, globalNotificationSys.DispatchNetOBDInfo(deadlinedCtx)...)
|
||||
obdInfo.Perf.NetParallel = globalNotificationSys.NetOBDParallelInfo(deadlinedCtx)
|
||||
partialWrite()
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
// ServerInfoHandler - GET /minio/admin/v2/info
|
||||
// ----------
|
||||
// Get server information
|
||||
func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ServerInfo")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, "")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.ServerInfoAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -1349,10 +1539,10 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
} else if ldapConn == nil {
|
||||
ldap.Status = "Not Configured"
|
||||
} else {
|
||||
// Close ldap connection to avoid leaks.
|
||||
ldapConn.Close()
|
||||
ldap.Status = "online"
|
||||
}
|
||||
// Close ldap connection to avoid leaks.
|
||||
defer ldapConn.Close()
|
||||
}
|
||||
|
||||
log, audit := fetchLoggerInfo(cfg)
|
||||
@@ -1361,7 +1551,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
notifyTarget := fetchLambdaInfo(cfg)
|
||||
|
||||
// Fetching the Storage information
|
||||
storageInfo := objectAPI.StorageInfo(ctx)
|
||||
storageInfo := objectAPI.StorageInfo(ctx, false)
|
||||
|
||||
var OnDisks int
|
||||
var OffDisks int
|
||||
@@ -1376,7 +1566,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
OffDisks += v
|
||||
}
|
||||
|
||||
backend = madmin.XlBackend{
|
||||
backend = madmin.XLBackend{
|
||||
Type: madmin.ErasureType,
|
||||
OnlineDisks: OnDisks,
|
||||
OfflineDisks: OffDisks,
|
||||
@@ -1386,7 +1576,7 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
RRSCParity: storageInfo.Backend.RRSCParity,
|
||||
}
|
||||
} else {
|
||||
backend = madmin.FsBackend{
|
||||
backend = madmin.FSBackend{
|
||||
Type: madmin.FsType,
|
||||
}
|
||||
}
|
||||
@@ -1470,10 +1660,13 @@ func (a adminAPIHandlers) ServerInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func fetchLambdaInfo(cfg config.Config) []map[string][]madmin.TargetIDStatus {
|
||||
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
||||
|
||||
// Fetch the targets
|
||||
targetList, _ := notify.RegisterNotificationTargets(cfg, GlobalServiceDoneCh, NewCustomHTTPTransport(), nil, true)
|
||||
targetList, err := notify.RegisterNotificationTargets(cfg, GlobalServiceDoneCh, NewGatewayHTTPTransport(), nil, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
lambdaMap := make(map[string][]madmin.TargetIDStatus)
|
||||
|
||||
for targetID, target := range targetList.TargetMap() {
|
||||
targetIDStatus := make(map[string]madmin.Status)
|
||||
@@ -1516,36 +1709,29 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
|
||||
return vault
|
||||
}
|
||||
|
||||
if err := checkConnection(kmsInfo.Endpoint); err != nil {
|
||||
if err := checkConnection(kmsInfo.Endpoint, 15*time.Second); err != nil {
|
||||
|
||||
vault.Status = "offline"
|
||||
} else {
|
||||
vault.Status = "online"
|
||||
|
||||
kmsContext := crypto.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||
kmsContext := crypto.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||
// 1. Generate a new key using the KMS.
|
||||
key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext)
|
||||
if err != nil {
|
||||
vault.Encrypt = "Encryption failed"
|
||||
vault.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
|
||||
} else {
|
||||
vault.Encrypt = "Ok"
|
||||
}
|
||||
|
||||
// 2. Check whether we can update / re-wrap the sealed key.
|
||||
sealedKey, err = GlobalKMS.UpdateKey(keyID, sealedKey, kmsContext)
|
||||
if err != nil {
|
||||
vault.Update = "Re-wrap failed:"
|
||||
} else {
|
||||
vault.Update = "Ok"
|
||||
}
|
||||
|
||||
// 3. Verify that we can indeed decrypt the (encrypted) key
|
||||
decryptedKey, decryptErr := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
|
||||
|
||||
// 4. Compare generated key with decrypted key
|
||||
if subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1 || decryptErr != nil {
|
||||
vault.Decrypt = "Re-wrap failed:"
|
||||
} else {
|
||||
// 2. Verify that we can indeed decrypt the (encrypted) key
|
||||
decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
|
||||
switch {
|
||||
case err != nil:
|
||||
vault.Decrypt = fmt.Sprintf("Decryption failed: %v", err)
|
||||
case subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1:
|
||||
vault.Decrypt = "Decryption failed: decrypted key does not match generated key"
|
||||
default:
|
||||
vault.Decrypt = "Ok"
|
||||
}
|
||||
}
|
||||
@@ -1560,7 +1746,7 @@ func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) {
|
||||
var auditlogger []madmin.Audit
|
||||
for log, l := range loggerCfg.HTTP {
|
||||
if l.Enabled {
|
||||
err := checkConnection(l.Endpoint)
|
||||
err := checkConnection(l.Endpoint, 15*time.Second)
|
||||
if err == nil {
|
||||
mapLog := make(map[string]madmin.Status)
|
||||
mapLog[log] = madmin.Status{Status: "Online"}
|
||||
@@ -1575,7 +1761,7 @@ func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) {
|
||||
|
||||
for audit, l := range loggerCfg.Audit {
|
||||
if l.Enabled {
|
||||
err := checkConnection(l.Endpoint)
|
||||
err := checkConnection(l.Endpoint, 15*time.Second)
|
||||
if err == nil {
|
||||
mapAudit := make(map[string]madmin.Status)
|
||||
mapAudit[audit] = madmin.Status{Status: "Online"}
|
||||
@@ -1591,12 +1777,19 @@ func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) {
|
||||
}
|
||||
|
||||
// checkConnection - ping an endpoint , return err in case of no connection
|
||||
func checkConnection(endpointStr string) error {
|
||||
func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||
u, pErr := xnet.ParseURL(endpointStr)
|
||||
if pErr != nil {
|
||||
return pErr
|
||||
}
|
||||
if dErr := u.DialHTTP(); dErr != nil {
|
||||
|
||||
tr := newCustomHTTPTransport(
|
||||
&tls.Config{RootCAs: globalRootCAs},
|
||||
timeout,
|
||||
0, /* Default value */
|
||||
)()
|
||||
|
||||
if dErr := u.DialHTTP(tr); dErr != nil {
|
||||
if urlErr, ok := dErr.(*url.Error); ok {
|
||||
// To treat "connection refused" errors as un reachable endpoint.
|
||||
if target.IsConnRefusedErr(urlErr.Err) {
|
||||
|
||||
@@ -109,14 +109,14 @@ func initTestXLObjLayer() (ObjectLayer, []string, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
endpoints := mustGetNewEndpoints(xlDirs...)
|
||||
format, err := waitForFormatXL(true, endpoints, 1, 16, "")
|
||||
storageDisks, format, err := waitForFormatXL(true, endpoints, 1, 1, 16, "")
|
||||
if err != nil {
|
||||
removeRoots(xlDirs)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
globalPolicySys = NewPolicySys()
|
||||
objLayer, err := newXLSets(endpoints, format, 1, 16)
|
||||
objLayer, err := newXLSets(endpoints, storageDisks, format, 1, 16)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -263,6 +263,7 @@ func TestAdminServerInfo(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
||||
}
|
||||
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
|
||||
+140
-36
@@ -106,12 +106,12 @@ func initHealState() *allHealState {
|
||||
healSeqMap: make(map[string]*healSequence),
|
||||
}
|
||||
|
||||
go healState.periodicHealSeqsClean()
|
||||
go healState.periodicHealSeqsClean(GlobalContext)
|
||||
|
||||
return healState
|
||||
}
|
||||
|
||||
func (ahs *allHealState) periodicHealSeqsClean() {
|
||||
func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||
// Launch clean-up routine to remove this heal sequence (after
|
||||
// it ends) from the global state after timeout has elapsed.
|
||||
ticker := time.NewTicker(time.Minute * 5)
|
||||
@@ -127,7 +127,7 @@ func (ahs *allHealState) periodicHealSeqsClean() {
|
||||
}
|
||||
}
|
||||
ahs.Unlock()
|
||||
case <-GlobalServiceDoneCh:
|
||||
case <-ctx.Done():
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
return
|
||||
@@ -306,6 +306,12 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
||||
return jbytes, ErrNone
|
||||
}
|
||||
|
||||
// healSource denotes single entity and heal option.
|
||||
type healSource struct {
|
||||
path string // entity path (format, buckets, objects) to heal
|
||||
opts *madmin.HealOpts // optional heal option overrides default setting
|
||||
}
|
||||
|
||||
// healSequence - state for each heal sequence initiated on the
|
||||
// server.
|
||||
type healSequence struct {
|
||||
@@ -316,11 +322,9 @@ type healSequence struct {
|
||||
path string
|
||||
|
||||
// List of entities (format, buckets, objects) to heal
|
||||
sourceCh chan string
|
||||
sourceCh chan healSource
|
||||
|
||||
// Report healing progress, false if this is a background
|
||||
// healing since currently there is no entity which will
|
||||
// receive realtime healing status
|
||||
// Report healing progress
|
||||
reportProgress bool
|
||||
|
||||
// time at which heal sequence was started
|
||||
@@ -352,14 +356,23 @@ type healSequence struct {
|
||||
// the last result index sent to client
|
||||
lastSentResultIndex int64
|
||||
|
||||
// Number of total items scanned
|
||||
scannedItemsCount int64
|
||||
// Number of total items scanned against item type
|
||||
scannedItemsMap map[madmin.HealItemType]int64
|
||||
|
||||
// Number of total items healed against item type
|
||||
healedItemsMap map[madmin.HealItemType]int64
|
||||
|
||||
// Number of total items where healing failed against endpoint and drive state
|
||||
healFailedItemsMap map[string]int64
|
||||
|
||||
// The time of the last scan/heal activity
|
||||
lastHealActivity time.Time
|
||||
|
||||
// Holds the request-info for logging
|
||||
ctx context.Context
|
||||
|
||||
// used to lock this structure as it is concurrently accessed
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewHealSequence - creates healSettings, assumes bucket and
|
||||
@@ -369,7 +382,7 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
||||
|
||||
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
||||
reqInfo.AppendTags("prefix", objPrefix)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
|
||||
return &healSequence{
|
||||
bucket: bucket,
|
||||
@@ -390,9 +403,83 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
||||
traverseAndHealDoneCh: make(chan error),
|
||||
stopSignalCh: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healFailedItemsMap: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
|
||||
// resetHealStatusCounters - reset the healSequence status counters between
|
||||
// each monthly background heal scanning activity.
|
||||
// This is used only in case of Background healing scenario, where
|
||||
// we use a single long running healSequence which reactively heals
|
||||
// objects passed to the SourceCh.
|
||||
func (h *healSequence) resetHealStatusCounters() {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.currentStatus.Items = []madmin.HealResultItem{}
|
||||
h.lastSentResultIndex = 0
|
||||
h.scannedItemsMap = make(map[madmin.HealItemType]int64)
|
||||
h.healedItemsMap = make(map[madmin.HealItemType]int64)
|
||||
h.healFailedItemsMap = make(map[string]int64)
|
||||
}
|
||||
|
||||
// getScannedItemsCount - returns a count of all scanned items
|
||||
func (h *healSequence) getScannedItemsCount() int64 {
|
||||
var count int64
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
for _, v := range h.scannedItemsMap {
|
||||
count = count + v
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getScannedItemsMap - returns map of all scanned items against type
|
||||
func (h *healSequence) getScannedItemsMap() map[madmin.HealItemType]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[madmin.HealItemType]int64, len(h.scannedItemsMap))
|
||||
for k, v := range h.scannedItemsMap {
|
||||
retMap[k] = v
|
||||
}
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
// getHealedItemsMap - returns the map of all healed items against type
|
||||
func (h *healSequence) getHealedItemsMap() map[madmin.HealItemType]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[madmin.HealItemType]int64, len(h.healedItemsMap))
|
||||
for k, v := range h.healedItemsMap {
|
||||
retMap[k] = v
|
||||
}
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
// gethealFailedItemsMap - returns map of all items where heal failed against
|
||||
// drive endpoint and status
|
||||
func (h *healSequence) gethealFailedItemsMap() map[string]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[string]int64, len(h.healFailedItemsMap))
|
||||
for k, v := range h.healFailedItemsMap {
|
||||
retMap[k] = v
|
||||
}
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
// isQuitting - determines if the heal sequence is quitting (due to an
|
||||
// external signal)
|
||||
func (h *healSequence) isQuitting() bool {
|
||||
@@ -548,14 +635,38 @@ func (h *healSequence) healSequenceStart() {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *healSequence) queueHealTask(path string, healType madmin.HealItemType) error {
|
||||
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
||||
var respCh = make(chan healResult)
|
||||
defer close(respCh)
|
||||
// Send heal request
|
||||
globalBackgroundHealRoutine.queueHealTask(healTask{path: path, responseCh: respCh, opts: h.settings})
|
||||
task := healTask{
|
||||
path: source.path,
|
||||
responseCh: respCh,
|
||||
opts: h.settings,
|
||||
}
|
||||
if source.opts != nil {
|
||||
task.opts = *source.opts
|
||||
}
|
||||
globalBackgroundHealRoutine.queueHealTask(task)
|
||||
// Wait for answer and push result to the client
|
||||
res := <-respCh
|
||||
if !h.reportProgress {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
// Progress is not reported in case of background heal processing.
|
||||
// Instead we increment relevant counter based on the heal result
|
||||
// for prometheus reporting.
|
||||
if res.err != nil && !isErrObjectNotFound(res.err) {
|
||||
for _, d := range res.result.After.Drives {
|
||||
// For failed items we report the endpoint and drive state
|
||||
// This will help users take corrective actions for drives
|
||||
h.healFailedItemsMap[d.Endpoint+","+d.State]++
|
||||
}
|
||||
} else {
|
||||
// Only object type reported for successful healing
|
||||
h.healedItemsMap[res.result.Type]++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
res.result.Type = healType
|
||||
@@ -575,8 +686,6 @@ func (h *healSequence) queueHealTask(path string, healType madmin.HealItemType)
|
||||
}
|
||||
|
||||
func (h *healSequence) healItemsFromSourceCh() error {
|
||||
h.lastHealActivity = UTCNow()
|
||||
|
||||
bucketsOnly := true // heal buckets only, not objects.
|
||||
if err := h.healItems(bucketsOnly); err != nil {
|
||||
logger.LogIf(h.ctx, err)
|
||||
@@ -584,28 +693,28 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
||||
|
||||
for {
|
||||
select {
|
||||
case path := <-h.sourceCh:
|
||||
case source := <-h.sourceCh:
|
||||
var itemType madmin.HealItemType
|
||||
switch {
|
||||
case path == nopHeal:
|
||||
case source.path == nopHeal:
|
||||
continue
|
||||
case path == SlashSeparator:
|
||||
case source.path == SlashSeparator:
|
||||
itemType = madmin.HealItemMetadata
|
||||
case !strings.Contains(path, SlashSeparator):
|
||||
case !strings.Contains(source.path, SlashSeparator):
|
||||
itemType = madmin.HealItemBucket
|
||||
default:
|
||||
itemType = madmin.HealItemObject
|
||||
}
|
||||
|
||||
if err := h.queueHealTask(path, itemType); err != nil {
|
||||
if err := h.queueHealTask(source, itemType); err != nil {
|
||||
logger.LogIf(h.ctx, err)
|
||||
}
|
||||
|
||||
h.scannedItemsCount++
|
||||
h.scannedItemsMap[itemType]++
|
||||
h.lastHealActivity = UTCNow()
|
||||
case <-h.traverseAndHealDoneCh:
|
||||
return nil
|
||||
case <-GlobalServiceDoneCh:
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -632,11 +741,6 @@ func (h *healSequence) healItems(bucketsOnly bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start healing the background ops prefix.
|
||||
if err := h.healMinioSysMeta(backgroundOpsMetaPrefix)(); err != nil {
|
||||
logger.LogIf(h.ctx, err)
|
||||
}
|
||||
|
||||
// Heal buckets and objects
|
||||
return h.healBuckets(bucketsOnly)
|
||||
}
|
||||
@@ -673,12 +777,12 @@ func (h *healSequence) healMinioSysMeta(metaPrefix string) func() error {
|
||||
// NOTE: Healing on meta is run regardless
|
||||
// of any bucket being selected, this is to ensure that
|
||||
// meta are always upto date and correct.
|
||||
return objectAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, func(bucket string, object string) error {
|
||||
return objectAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket string, object string) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
herr := h.queueHealTask(pathJoin(bucket, object), madmin.HealItemBucketMetadata)
|
||||
herr := h.queueHealTask(healSource{path: pathJoin(bucket, object)}, madmin.HealItemBucketMetadata)
|
||||
// Object might have been deleted, by the time heal
|
||||
// was attempted we ignore this object an move on.
|
||||
if isErrObjectNotFound(herr) {
|
||||
@@ -702,7 +806,7 @@ func (h *healSequence) healDiskFormat() error {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
return h.queueHealTask(SlashSeparator, madmin.HealItemMetadata)
|
||||
return h.queueHealTask(healSource{path: SlashSeparator}, madmin.HealItemMetadata)
|
||||
}
|
||||
|
||||
// healBuckets - check for all buckets heal or just particular bucket.
|
||||
@@ -744,7 +848,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
if err := h.queueHealTask(bucket, madmin.HealItemBucket); err != nil {
|
||||
if err := h.queueHealTask(healSource{path: bucket}, madmin.HealItemBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -767,7 +871,7 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := objectAPI.HealObjects(h.ctx, bucket, h.objPrefix, h.healObject); err != nil {
|
||||
if err := objectAPI.HealObjects(h.ctx, bucket, h.objPrefix, h.settings, h.healObject); err != nil {
|
||||
return errFnHealFromAPIErr(h.ctx, err)
|
||||
}
|
||||
return nil
|
||||
@@ -775,15 +879,15 @@ func (h *healSequence) healBucket(bucket string, bucketsOnly bool) error {
|
||||
|
||||
// healObject - heal the given object and record result
|
||||
func (h *healSequence) healObject(bucket, object string) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
return h.queueHealTask(pathJoin(bucket, object), madmin.HealItemObject)
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
return h.queueHealTask(healSource{path: pathJoin(bucket, object)}, madmin.HealItemObject)
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
// DataUsageInfo operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/datausageinfo").HandlerFunc(httpTraceAll(adminAPI.DataUsageInfoHandler))
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/accountingusageinfo").HandlerFunc(httpTraceAll(adminAPI.AccountingUsageInfoHandler))
|
||||
|
||||
if globalIsDistXL || globalIsXL {
|
||||
/// Heal operations
|
||||
|
||||
@@ -108,6 +110,10 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix+"/set-user-status").HandlerFunc(httpTraceHdrs(adminAPI.SetUserStatus)).
|
||||
Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
// Service accounts ops
|
||||
adminRouter.Methods(http.MethodPut).Path(adminAPIVersionPrefix + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/get-service-account").HandlerFunc(httpTraceHdrs(adminAPI.GetServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// Info policy IAM
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/info-canned-policy").HandlerFunc(httpTraceHdrs(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
||||
|
||||
@@ -160,6 +166,12 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
//
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix + "/kms/key/status").HandlerFunc(httpTraceAll(adminAPI.KMSKeyStatusHandler))
|
||||
|
||||
if !globalIsGateway {
|
||||
|
||||
// -- OBD API --
|
||||
adminRouter.Methods(http.MethodGet).Path(adminAPIVersionPrefix+"/obdinfo").HandlerFunc(httpTraceHdrs(adminAPI.OBDInfoHandler)).Queries("perfdrive", "{perfdrive:true|false}", "perfnet", "{perfnet:true|false}", "minioinfo", "{minioinfo:true|false}", "minioconfig", "{minioconfig:true|false}", "syscpu", "{syscpu:true|false}", "sysdiskhw", "{sysdiskhw:true|false}", "sysosinfo", "{sysosinfo:true|false}", "sysmem", "{sysmem:true|false}", "sysprocess", "{sysprocess:true|false}")
|
||||
}
|
||||
|
||||
// If none of the routes match add default error handler routes
|
||||
adminRouter.NotFoundHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler))
|
||||
adminRouter.MethodNotAllowedHandler = http.HandlerFunc(httpTraceAll(errorResponseHandler))
|
||||
|
||||
+30
-43
@@ -20,7 +20,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/cpu"
|
||||
@@ -190,51 +189,49 @@ func getLocalNetworkInfo(endpointZones EndpointZones, r *http.Request) madmin.Se
|
||||
// getLocalServerProperty - returns ServerDrivesPerfInfo for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalServerProperty(endpointZones EndpointZones, r *http.Request) madmin.ServerProperties {
|
||||
var di madmin.Disk
|
||||
var disks []madmin.Disk
|
||||
addr := r.Host
|
||||
if globalIsDistXL {
|
||||
addr = GetLocalPeer(endpointZones)
|
||||
}
|
||||
network := make(map[string]string)
|
||||
hosts := set.NewStringSet()
|
||||
for _, ep := range endpointZones {
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
|
||||
url := strings.Replace(endpoint.URL.String(), endpoint.Path, "", -1)
|
||||
if url == "" {
|
||||
url = r.Host
|
||||
nodeName := endpoint.Host
|
||||
if nodeName == "" {
|
||||
nodeName = r.Host
|
||||
}
|
||||
hosts.Add(url)
|
||||
|
||||
// Only proceed for local endpoints
|
||||
if endpoint.IsLocal {
|
||||
url = fetchAddress(url)
|
||||
network[url] = "online"
|
||||
if _, err := os.Stat(endpoint.Path); err != nil {
|
||||
continue
|
||||
// Only proceed for local endpoints
|
||||
network[nodeName] = "online"
|
||||
var di = madmin.Disk{
|
||||
DrivePath: endpoint.Path,
|
||||
}
|
||||
diInfo, err := disk.GetInfo(endpoint.Path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || isSysErrPathNotFound(err) {
|
||||
di.State = madmin.DriveStateMissing
|
||||
} else {
|
||||
di.State = madmin.DriveStateCorrupt
|
||||
}
|
||||
} else {
|
||||
di.State = madmin.DriveStateOk
|
||||
di.DrivePath = endpoint.Path
|
||||
di.TotalSpace = diInfo.Total
|
||||
di.UsedSpace = diInfo.Total - diInfo.Free
|
||||
di.Utilization = float64((diInfo.Total - diInfo.Free) / diInfo.Total * 100)
|
||||
}
|
||||
|
||||
diInfo, _ := disk.GetInfo(endpoint.Path)
|
||||
di.State = "ok"
|
||||
di.DrivePath = endpoint.Path
|
||||
di.TotalSpace = diInfo.Total
|
||||
di.UsedSpace = diInfo.Total - diInfo.Free
|
||||
di.Utilization = float64((diInfo.Total - diInfo.Free) / diInfo.Total * 100)
|
||||
disks = append(disks, di)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for host := range hosts {
|
||||
_, present := network[host]
|
||||
if !present {
|
||||
err := checkConnection(host)
|
||||
host = fetchAddress(host)
|
||||
if err != nil {
|
||||
network[host] = "offline"
|
||||
} else {
|
||||
network[host] = "online"
|
||||
_, present := network[nodeName]
|
||||
if !present {
|
||||
err := IsServerResolvable(endpoint)
|
||||
if err == nil {
|
||||
network[nodeName] = "online"
|
||||
} else {
|
||||
network[nodeName] = "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,13 +246,3 @@ func getLocalServerProperty(endpointZones EndpointZones, r *http.Request) madmin
|
||||
Disks: disks,
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces http and https from address
|
||||
func fetchAddress(address string) string {
|
||||
if strings.Contains(address, "http://") {
|
||||
address = strings.Replace(address, "http://", "", -1)
|
||||
} else if strings.Contains(address, "https://") {
|
||||
address = strings.Replace(address, "https://", "", -1)
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
+96
-11
@@ -21,6 +21,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
@@ -32,9 +33,12 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
// APIError structure
|
||||
@@ -91,9 +95,12 @@ const (
|
||||
ErrMissingContentLength
|
||||
ErrMissingContentMD5
|
||||
ErrMissingRequestBodyError
|
||||
ErrMissingSecurityHeader
|
||||
ErrNoSuchBucket
|
||||
ErrNoSuchBucketPolicy
|
||||
ErrNoSuchBucketLifecycle
|
||||
ErrNoSuchLifecycleConfiguration
|
||||
ErrNoSuchBucketSSEConfig
|
||||
ErrNoSuchKey
|
||||
ErrNoSuchUpload
|
||||
ErrNoSuchVersion
|
||||
@@ -115,7 +122,8 @@ const (
|
||||
ErrMissingCredTag
|
||||
ErrCredMalformed
|
||||
ErrInvalidRegion
|
||||
ErrInvalidService
|
||||
ErrInvalidServiceS3
|
||||
ErrInvalidServiceSTS
|
||||
ErrInvalidRequestVersion
|
||||
ErrMissingSignTag
|
||||
ErrMissingSignHeadersTag
|
||||
@@ -144,11 +152,13 @@ const (
|
||||
ErrKeyTooLongError
|
||||
ErrInvalidBucketObjectLockConfiguration
|
||||
ErrObjectLockConfigurationNotAllowed
|
||||
ErrNoSuchObjectLockConfiguration
|
||||
ErrObjectLocked
|
||||
ErrInvalidRetentionDate
|
||||
ErrPastObjectLockRetainDate
|
||||
ErrUnknownWORMModeDirective
|
||||
ErrObjectLockInvalidHeaders
|
||||
ErrInvalidTagDirective
|
||||
// Add new error codes here.
|
||||
|
||||
// SSE-S3 related API errors
|
||||
@@ -201,6 +211,7 @@ const (
|
||||
ErrInvalidResourceName
|
||||
ErrServerNotInitialized
|
||||
ErrOperationTimedOut
|
||||
ErrOperationMaxedOut
|
||||
ErrInvalidRequest
|
||||
// MinIO storage class error codes
|
||||
ErrInvalidStorageClass
|
||||
@@ -323,6 +334,7 @@ const (
|
||||
ErrAdminProfilerNotEnabled
|
||||
ErrInvalidDecompressedSize
|
||||
ErrAddUserInvalidArgument
|
||||
ErrAddServiceAccountInvalidArgument
|
||||
ErrPostPolicyConditionInvalidFormat
|
||||
)
|
||||
|
||||
@@ -464,6 +476,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Missing required header for this request: Content-Md5.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSecurityHeader: {
|
||||
Code: "MissingSecurityHeader",
|
||||
Description: "Your request was missing a required header",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingRequestBodyError: {
|
||||
Code: "MissingRequestBodyError",
|
||||
Description: "Request body is empty.",
|
||||
@@ -484,6 +501,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "The bucket lifecycle configuration does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchLifecycleConfiguration: {
|
||||
Code: "NoSuchLifecycleConfiguration",
|
||||
Description: "The lifecycle configuration does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchBucketSSEConfig: {
|
||||
Code: "ServerSideEncryptionConfigurationNotFoundError",
|
||||
Description: "The server side encryption configuration was not found",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrNoSuchKey: {
|
||||
Code: "NoSuchKey",
|
||||
Description: "The specified key does not exist.",
|
||||
@@ -628,9 +655,14 @@ var errorCodes = errorCodeMap{
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
// right Description: "Error parsing the X-Amz-Credential parameter; incorrect service \"s4\". This endpoint belongs to \"s3\".".
|
||||
// Need changes to make sure variable messages can be constructed.
|
||||
ErrInvalidService: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "Error parsing the X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
||||
ErrInvalidServiceS3: {
|
||||
Code: "AuthorizationParametersError",
|
||||
Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidServiceSTS: {
|
||||
Code: "AuthorizationParametersError",
|
||||
Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// FIXME: Should contain the invalid param set as seen in https://github.com/minio/minio/issues/2385.
|
||||
@@ -738,6 +770,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Object Lock configuration cannot be enabled on existing buckets.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrNoSuchObjectLockConfiguration: {
|
||||
Code: "NoSuchObjectLockConfiguration",
|
||||
Description: "The specified object does not have a ObjectLock configuration",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrObjectLocked: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "Object is WORM protected and cannot be overwritten",
|
||||
@@ -829,6 +866,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Your metadata headers exceed the maximum allowed metadata size.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTagDirective: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Unknown tag directive.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidEncryptionMethod: {
|
||||
Code: "InvalidRequest",
|
||||
Description: "The encryption method specified is not supported",
|
||||
@@ -1044,6 +1086,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "A timeout occurred while trying to lock a resource",
|
||||
HTTPStatusCode: http.StatusRequestTimeout,
|
||||
},
|
||||
ErrOperationMaxedOut: {
|
||||
Code: "XMinioServerTimedOut",
|
||||
Description: "A timeout exceeded while waiting to proceed with the request",
|
||||
HTTPStatusCode: http.StatusRequestTimeout,
|
||||
},
|
||||
ErrUnsupportedMetadata: {
|
||||
Code: "InvalidArgument",
|
||||
Description: "Your metadata headers are not supported.",
|
||||
@@ -1539,6 +1586,12 @@ var errorCodes = errorCodeMap{
|
||||
Description: "User is not allowed to be same as admin access key",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAddServiceAccountInvalidArgument: {
|
||||
Code: "XMinioInvalidArgument",
|
||||
Description: "New service accounts for admin access key is not allowed",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
|
||||
ErrPostPolicyConditionInvalidFormat: {
|
||||
Code: "PostPolicyInvalidKeyName",
|
||||
Description: "Invalid according to Policy: Policy Condition failed",
|
||||
@@ -1607,18 +1660,20 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrKMSNotConfigured
|
||||
case crypto.ErrKMSAuthLogin:
|
||||
apiErr = ErrKMSAuthFailure
|
||||
case errOperationTimedOut, context.Canceled, context.DeadlineExceeded:
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
apiErr = ErrOperationTimedOut
|
||||
case errDiskNotFound:
|
||||
apiErr = ErrSlowDown
|
||||
case errInvalidRetentionDate:
|
||||
case objectlock.ErrInvalidRetentionDate:
|
||||
apiErr = ErrInvalidRetentionDate
|
||||
case errPastObjectLockRetainDate:
|
||||
case objectlock.ErrPastObjectLockRetainDate:
|
||||
apiErr = ErrPastObjectLockRetainDate
|
||||
case errUnknownWORMModeDirective:
|
||||
case objectlock.ErrUnknownWORMModeDirective:
|
||||
apiErr = ErrUnknownWORMModeDirective
|
||||
case errObjectLockInvalidHeaders:
|
||||
case objectlock.ErrObjectLockInvalidHeaders:
|
||||
apiErr = ErrObjectLockInvalidHeaders
|
||||
case objectlock.ErrMalformedXML:
|
||||
apiErr = ErrMalformedXML
|
||||
}
|
||||
|
||||
// Compression errors
|
||||
@@ -1708,7 +1763,9 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
case BucketPolicyNotFound:
|
||||
apiErr = ErrNoSuchBucketPolicy
|
||||
case BucketLifecycleNotFound:
|
||||
apiErr = ErrNoSuchBucketLifecycle
|
||||
apiErr = ErrNoSuchLifecycleConfiguration
|
||||
case BucketSSEConfigNotFound:
|
||||
apiErr = ErrNoSuchBucketSSEConfig
|
||||
case *event.ErrInvalidEventName:
|
||||
apiErr = ErrEventNotification
|
||||
case *event.ErrInvalidARN:
|
||||
@@ -1731,6 +1788,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrOverlappingFilterNotification
|
||||
case *event.ErrUnsupportedConfiguration:
|
||||
apiErr = ErrUnsupportedNotification
|
||||
case OperationTimedOut:
|
||||
apiErr = ErrOperationTimedOut
|
||||
case BackendDown:
|
||||
apiErr = ErrBackendDown
|
||||
case ObjectNameTooLong:
|
||||
@@ -1777,6 +1836,32 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
// their internal error types. This code is only
|
||||
// useful with gateway implementations.
|
||||
switch e := err.(type) {
|
||||
case *xml.SyntaxError:
|
||||
apiErr = APIError{
|
||||
Code: "MalformedXML",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description,
|
||||
e.Error()),
|
||||
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||
}
|
||||
case url.EscapeError:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioInvalidObjectName",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description,
|
||||
e.Error()),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case lifecycle.Error:
|
||||
apiErr = APIError{
|
||||
Code: "InvalidRequest",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case tagging.Error:
|
||||
apiErr = APIError{
|
||||
Code: e.Code(),
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case policy.Error:
|
||||
apiErr = APIError{
|
||||
Code: "MalformedPolicy",
|
||||
|
||||
@@ -22,7 +22,9 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
@@ -80,6 +82,10 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
w.Header()[xhttp.ETag] = []string{"\"" + objInfo.ETag + "\""}
|
||||
}
|
||||
|
||||
if strings.Contains(objInfo.ETag, "-") && len(objInfo.Parts) > 0 {
|
||||
w.Header().Set(xhttp.AmzMpPartsCount, strconv.Itoa(len(objInfo.Parts)))
|
||||
}
|
||||
|
||||
if objInfo.ContentType != "" {
|
||||
w.Header().Set(xhttp.ContentType, objInfo.ContentType)
|
||||
}
|
||||
@@ -95,6 +101,14 @@ func setObjectHeaders(w http.ResponseWriter, objInfo ObjectInfo, rs *HTTPRangeSp
|
||||
w.Header().Set(xhttp.XCache, objInfo.CacheStatus.String())
|
||||
w.Header().Set(xhttp.XCacheLookup, objInfo.CacheLookupStatus.String())
|
||||
}
|
||||
|
||||
// Set tag count if object has tags
|
||||
tags, _ := url.ParseQuery(objInfo.UserTags)
|
||||
tagCount := len(tags)
|
||||
if tagCount != 0 {
|
||||
w.Header().Set(xhttp.AmzTagCount, strconv.Itoa(tagCount))
|
||||
}
|
||||
|
||||
// Set all other user defined metadata.
|
||||
for k, v := range objInfo.UserDefined {
|
||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||
|
||||
+16
-3
@@ -429,7 +429,12 @@ func generateListVersionsResponse(bucket, prefix, marker, delimiter, encodingTyp
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = object.StorageClass
|
||||
if object.StorageClass != "" {
|
||||
content.StorageClass = object.StorageClass
|
||||
} else {
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
}
|
||||
|
||||
content.Owner = owner
|
||||
content.VersionID = "null"
|
||||
content.IsLatest = true
|
||||
@@ -475,7 +480,11 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingTy
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = object.StorageClass
|
||||
if object.StorageClass != "" {
|
||||
content.StorageClass = object.StorageClass
|
||||
} else {
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
}
|
||||
content.Owner = owner
|
||||
contents = append(contents, content)
|
||||
}
|
||||
@@ -521,7 +530,11 @@ func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter,
|
||||
content.ETag = "\"" + object.ETag + "\""
|
||||
}
|
||||
content.Size = object.Size
|
||||
content.StorageClass = object.StorageClass
|
||||
if object.StorageClass != "" {
|
||||
content.StorageClass = object.StorageClass
|
||||
} else {
|
||||
content.StorageClass = globalMinioDefaultStorageClass
|
||||
}
|
||||
content.Owner = owner
|
||||
if metadata {
|
||||
content.UserMetadata = make(StringMap)
|
||||
|
||||
+169
-61
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2016 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2016-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,9 +18,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
func newHTTPServerFn() *xhttp.Server {
|
||||
@@ -87,126 +92,229 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
|
||||
}
|
||||
routers = append(routers, apiRouter.PathPrefix("/{bucket}").Subrouter())
|
||||
|
||||
requestsMax, _ := strconv.Atoi(env.Get(config.EnvAPIRequestsMax, "0"))
|
||||
if requestsMax < 0 {
|
||||
requestsMax = 0 // 0 means unlimited
|
||||
}
|
||||
|
||||
var (
|
||||
enabled bool
|
||||
requestsMaxPerServer int
|
||||
requestsDeadline time.Duration
|
||||
)
|
||||
|
||||
if len(globalEndpoints.Hosts()) == 0 {
|
||||
requestsMaxPerServer = requestsMax
|
||||
} else {
|
||||
requestsMaxPerServer = requestsMax / len(globalEndpoints.Hosts())
|
||||
}
|
||||
|
||||
enabled = requestsMaxPerServer > 0
|
||||
if enabled {
|
||||
var err error
|
||||
requestsDeadline, err = time.ParseDuration(env.Get(config.EnvAPIRequestsDeadline, "10s"))
|
||||
if err != nil {
|
||||
logger.Info("(%s) parsing environment value MINIO_API_REQUESTS_DEADLINE, defaulting to 10 seconds", err)
|
||||
requestsDeadline = 10 * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
requestsMaxCh := make(chan struct{}, requestsMaxPerServer)
|
||||
|
||||
for _, bucket := range routers {
|
||||
// Object operations
|
||||
// HeadObject
|
||||
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler)))
|
||||
bucket.Methods(http.MethodHead).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("headobject", httpTraceAll(api.HeadObjectHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// CopyObjectPart
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobjectpart", httpTraceAll(api.CopyObjectPartHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// PutObjectPart
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler))).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobjectpart", httpTraceHdrs(api.PutObjectPartHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("partNumber", "{partNumber:[0-9]+}", "uploadId", "{uploadId:.*}")
|
||||
// ListObjectParts
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("listobjectparts", httpTraceAll(api.ListObjectPartsHandler))).Queries("uploadId", "{uploadId:.*}")
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("listobjectparts", httpTraceAll(api.ListObjectPartsHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("uploadId", "{uploadId:.*}")
|
||||
// CompleteMultipartUpload
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("completemutipartupload", httpTraceAll(api.CompleteMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("completemutipartupload", httpTraceAll(api.CompleteMultipartUploadHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("uploadId", "{uploadId:.*}")
|
||||
// NewMultipartUpload
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("newmultipartupload", httpTraceAll(api.NewMultipartUploadHandler))).Queries("uploads", "")
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("newmultipartupload", httpTraceAll(api.NewMultipartUploadHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("uploads", "")
|
||||
// AbortMultipartUpload
|
||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler))).Queries("uploadId", "{uploadId:.*}")
|
||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("abortmultipartupload", httpTraceAll(api.AbortMultipartUploadHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("uploadId", "{uploadId:.*}")
|
||||
// GetObjectACL - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler))).Queries("acl", "")
|
||||
// GetObjectTagging - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler))).Queries("tagging", "")
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("getobjectacl", httpTraceHdrs(api.GetObjectACLHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("acl", "")
|
||||
// PutObjectACL - this is a dummy call.
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobjectacl", httpTraceHdrs(api.PutObjectACLHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("acl", "")
|
||||
// GetObjectTagging
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("getobjecttagging", httpTraceHdrs(api.GetObjectTaggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("tagging", "")
|
||||
// PutObjectTagging
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobjecttagging", httpTraceHdrs(api.PutObjectTaggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("tagging", "")
|
||||
// DeleteObjectTagging
|
||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("deleteobjecttagging", httpTraceHdrs(api.DeleteObjectTaggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("tagging", "")
|
||||
// SelectObjectContent
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler))).Queries("select", "").Queries("select-type", "2")
|
||||
bucket.Methods(http.MethodPost).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("selectobjectcontent", httpTraceHdrs(api.SelectObjectContentHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("select", "").Queries("select-type", "2")
|
||||
// GetObjectRetention
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectretention", httpTraceHdrs(api.GetObjectRetentionHandler))).Queries("retention", "")
|
||||
// GetObject
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)))
|
||||
// CopyObject
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)))
|
||||
// PutObjectRetention
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectretention", httpTraceHdrs(api.PutObjectRetentionHandler))).Queries("retention", "")
|
||||
// PutObject
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler)))
|
||||
// DeleteObject
|
||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler)))
|
||||
// PutObjectLegalHold
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(collectAPIStats("putobjectlegalhold", httpTraceHdrs(api.PutObjectLegalHoldHandler))).Queries("legal-hold", "")
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("getobjectretention", httpTraceAll(api.GetObjectRetentionHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("retention", "")
|
||||
// GetObjectLegalHold
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(collectAPIStats("getobjectlegalhold", httpTraceHdrs(api.GetObjectLegalHoldHandler))).Queries("legal-hold", "")
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("getobjectlegalhold", httpTraceAll(api.GetObjectLegalHoldHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("legal-hold", "")
|
||||
// GetObject
|
||||
bucket.Methods(http.MethodGet).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("getobject", httpTraceHdrs(api.GetObjectHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// CopyObject
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HeadersRegexp(xhttp.AmzCopySource, ".*?(\\/|%2F).*?").HandlerFunc(maxClients(collectAPIStats("copyobject", httpTraceAll(api.CopyObjectHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// PutObjectRetention
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobjectretention", httpTraceAll(api.PutObjectRetentionHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("retention", "")
|
||||
// PutObjectLegalHold
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobjectlegalhold", httpTraceAll(api.PutObjectLegalHoldHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("legal-hold", "")
|
||||
|
||||
// PutObject
|
||||
bucket.Methods(http.MethodPut).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("putobject", httpTraceHdrs(api.PutObjectHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// DeleteObject
|
||||
bucket.Methods(http.MethodDelete).Path("/{object:.+}").HandlerFunc(
|
||||
maxClients(collectAPIStats("deleteobject", httpTraceAll(api.DeleteObjectHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
|
||||
/// Bucket operations
|
||||
// GetBucketLocation
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlocation", httpTraceAll(api.GetBucketLocationHandler))).Queries("location", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketlocation", httpTraceAll(api.GetBucketLocationHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("location", "")
|
||||
// GetBucketPolicy
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketpolicy", httpTraceAll(api.GetBucketPolicyHandler))).Queries("policy", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketpolicy", httpTraceAll(api.GetBucketPolicyHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("policy", "")
|
||||
// GetBucketLifecycle
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler))).Queries("lifecycle", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("lifecycle", "")
|
||||
// GetBucketEncryption
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketencryption", httpTraceAll(api.GetBucketEncryptionHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("encryption", "")
|
||||
|
||||
// Dummy Bucket Calls
|
||||
// GetBucketACL -- this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketacl", httpTraceAll(api.GetBucketACLHandler))).Queries("acl", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketacl", httpTraceAll(api.GetBucketACLHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("acl", "")
|
||||
// PutBucketACL -- this is a dummy call.
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketacl", httpTraceAll(api.PutBucketACLHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("acl", "")
|
||||
// GetBucketCors - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketcors", httpTraceAll(api.GetBucketCorsHandler))).Queries("cors", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketcors", httpTraceAll(api.GetBucketCorsHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("cors", "")
|
||||
// GetBucketWebsiteHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketwebsite", httpTraceAll(api.GetBucketWebsiteHandler))).Queries("website", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketwebsite", httpTraceAll(api.GetBucketWebsiteHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("website", "")
|
||||
// GetBucketAccelerateHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketaccelerate", httpTraceAll(api.GetBucketAccelerateHandler))).Queries("accelerate", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketaccelerate", httpTraceAll(api.GetBucketAccelerateHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("accelerate", "")
|
||||
// GetBucketRequestPaymentHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketrequestpayment", httpTraceAll(api.GetBucketRequestPaymentHandler))).Queries("requestPayment", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketrequestpayment", httpTraceAll(api.GetBucketRequestPaymentHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("requestPayment", "")
|
||||
// GetBucketLoggingHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlogging", httpTraceAll(api.GetBucketLoggingHandler))).Queries("logging", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketlogging", httpTraceAll(api.GetBucketLoggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("logging", "")
|
||||
// GetBucketLifecycleHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler))).Queries("lifecycle", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketlifecycle", httpTraceAll(api.GetBucketLifecycleHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("lifecycle", "")
|
||||
// GetBucketReplicationHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler))).Queries("replication", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("replication", "")
|
||||
// GetBucketTaggingHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler))).Queries("tagging", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("tagging", "")
|
||||
//DeleteBucketWebsiteHandler
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketwebsite", httpTraceAll(api.DeleteBucketWebsiteHandler))).Queries("website", "")
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebucketwebsite", httpTraceAll(api.DeleteBucketWebsiteHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("website", "")
|
||||
// DeleteBucketTaggingHandler
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebuckettagging", httpTraceAll(api.DeleteBucketTaggingHandler))).Queries("tagging", "")
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebuckettagging", httpTraceAll(api.DeleteBucketTaggingHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("tagging", "")
|
||||
|
||||
// GetBucketObjectLockConfig
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler))).Queries("object-lock", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketobjectlockconfiguration", httpTraceAll(api.GetBucketObjectLockConfigHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("object-lock", "")
|
||||
// GetBucketVersioning
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler))).Queries("versioning", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketversioning", httpTraceAll(api.GetBucketVersioningHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("versioning", "")
|
||||
// GetBucketNotification
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler))).Queries("notification", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketnotification", httpTraceAll(api.GetBucketNotificationHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("notification", "")
|
||||
// ListenBucketNotification
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listenbucketnotification", httpTraceAll(api.ListenBucketNotificationHandler))).Queries("events", "{events:.*}")
|
||||
// ListMultipartUploads
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listmultipartuploads", httpTraceAll(api.ListMultipartUploadsHandler))).Queries("uploads", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("listmultipartuploads", httpTraceAll(api.ListMultipartUploadsHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("uploads", "")
|
||||
// ListObjectsV2M
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv2M", httpTraceAll(api.ListObjectsV2MHandler))).Queries("list-type", "2", "metadata", "true")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("listobjectsv2M", httpTraceAll(api.ListObjectsV2MHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("list-type", "2", "metadata", "true")
|
||||
// ListObjectsV2
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv2", httpTraceAll(api.ListObjectsV2Handler))).Queries("list-type", "2")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("listobjectsv2", httpTraceAll(api.ListObjectsV2Handler)), enabled, requestsMaxCh, requestsDeadline)).Queries("list-type", "2")
|
||||
// ListBucketVersions
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listbucketversions", httpTraceAll(api.ListBucketObjectVersionsHandler))).Queries("versions", "")
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("listbucketversions", httpTraceAll(api.ListBucketObjectVersionsHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("versions", "")
|
||||
// ListObjectsV1 (Legacy)
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(collectAPIStats("listobjectsv1", httpTraceAll(api.ListObjectsV1Handler)))
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("listobjectsv1", httpTraceAll(api.ListObjectsV1Handler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// PutBucketLifecycle
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler))).Queries("lifecycle", "")
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketlifecycle", httpTraceAll(api.PutBucketLifecycleHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("lifecycle", "")
|
||||
// PutBucketEncryption
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketencryption", httpTraceAll(api.PutBucketEncryptionHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("encryption", "")
|
||||
|
||||
// PutBucketPolicy
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketpolicy", httpTraceAll(api.PutBucketPolicyHandler))).Queries("policy", "")
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketpolicy", httpTraceAll(api.PutBucketPolicyHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("policy", "")
|
||||
|
||||
// PutBucketObjectLockConfig
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketobjectlockconfig", httpTraceAll(api.PutBucketObjectLockConfigHandler))).Queries("object-lock", "")
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketobjectlockconfig", httpTraceAll(api.PutBucketObjectLockConfigHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("object-lock", "")
|
||||
// PutBucketVersioning
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketversioning", httpTraceAll(api.PutBucketVersioningHandler))).Queries("versioning", "")
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketversioning", httpTraceAll(api.PutBucketVersioningHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("versioning", "")
|
||||
// PutBucketNotification
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucketnotification", httpTraceAll(api.PutBucketNotificationHandler))).Queries("notification", "")
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketnotification", httpTraceAll(api.PutBucketNotificationHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("notification", "")
|
||||
// PutBucket
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(collectAPIStats("putbucket", httpTraceAll(api.PutBucketHandler)))
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucket", httpTraceAll(api.PutBucketHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// HeadBucket
|
||||
bucket.Methods(http.MethodHead).HandlerFunc(collectAPIStats("headbucket", httpTraceAll(api.HeadBucketHandler)))
|
||||
bucket.Methods(http.MethodHead).HandlerFunc(
|
||||
maxClients(collectAPIStats("headbucket", httpTraceAll(api.HeadBucketHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// PostPolicy
|
||||
bucket.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(collectAPIStats("postpolicybucket", httpTraceHdrs(api.PostPolicyBucketHandler)))
|
||||
bucket.Methods(http.MethodPost).HeadersRegexp(xhttp.ContentType, "multipart/form-data*").HandlerFunc(
|
||||
maxClients(collectAPIStats("postpolicybucket", httpTraceHdrs(api.PostPolicyBucketHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
// DeleteMultipleObjects
|
||||
bucket.Methods(http.MethodPost).HandlerFunc(collectAPIStats("deletemultipleobjects", httpTraceAll(api.DeleteMultipleObjectsHandler))).Queries("delete", "")
|
||||
bucket.Methods(http.MethodPost).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletemultipleobjects", httpTraceAll(api.DeleteMultipleObjectsHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("delete", "")
|
||||
// DeleteBucketPolicy
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler))).Queries("policy", "")
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebucketpolicy", httpTraceAll(api.DeleteBucketPolicyHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("policy", "")
|
||||
// DeleteBucketLifecycle
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler))).Queries("lifecycle", "")
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebucketlifecycle", httpTraceAll(api.DeleteBucketLifecycleHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("lifecycle", "")
|
||||
// DeleteBucketEncryption
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebucketencryption", httpTraceAll(api.DeleteBucketEncryptionHandler)), enabled, requestsMaxCh, requestsDeadline)).Queries("encryption", "")
|
||||
// DeleteBucket
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(collectAPIStats("deletebucket", httpTraceAll(api.DeleteBucketHandler)))
|
||||
bucket.Methods(http.MethodDelete).HandlerFunc(
|
||||
maxClients(collectAPIStats("deletebucket", httpTraceAll(api.DeleteBucketHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
}
|
||||
|
||||
/// Root operation
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler)))
|
||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||
maxClients(collectAPIStats("listbuckets", httpTraceAll(api.ListBucketsHandler)), enabled, requestsMaxCh, requestsDeadline))
|
||||
|
||||
// If none of the routes match add default error handler routes
|
||||
apiRouter.NotFoundHandler = http.HandlerFunc(collectAPIStats("notfound", httpTraceAll(errorResponseHandler)))
|
||||
|
||||
+34
-56
@@ -28,13 +28,13 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
jwtgo "github.com/dgrijalva/jwt-go"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
xjwt "github.com/minio/minio/cmd/jwt"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
// Verify if request has JWT.
|
||||
@@ -179,12 +179,14 @@ func mustGetClaimsFromToken(r *http.Request) map[string]interface{} {
|
||||
|
||||
// Fetch claims in the security token returned by the client.
|
||||
func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
||||
claims := make(map[string]interface{})
|
||||
claims := xjwt.NewMapClaims()
|
||||
|
||||
token := getSessionToken(r)
|
||||
if token == "" {
|
||||
return claims, nil
|
||||
return claims.Map(), nil
|
||||
}
|
||||
stsTokenCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
|
||||
|
||||
stsTokenCallback := func(claims *xjwt.MapClaims) ([]byte, error) {
|
||||
// JWT token for x-amz-security-token is signed with admin
|
||||
// secret key, temporary credentials become invalid if
|
||||
// server admin credentials change. This is done to ensure
|
||||
@@ -195,66 +197,43 @@ func getClaimsFromToken(r *http.Request) (map[string]interface{}, error) {
|
||||
// on the client side and is treated like an opaque value.
|
||||
return []byte(globalActiveCred.SecretKey), nil
|
||||
}
|
||||
p := &jwtgo.Parser{
|
||||
ValidMethods: []string{
|
||||
jwtgo.SigningMethodHS256.Alg(),
|
||||
jwtgo.SigningMethodHS512.Alg(),
|
||||
},
|
||||
}
|
||||
jtoken, err := p.ParseWithClaims(token, jwtgo.MapClaims(claims), stsTokenCallback)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !jtoken.Valid {
|
||||
|
||||
if err := xjwt.ParseWithClaims(token, claims, stsTokenCallback); err != nil {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
v, ok := claims["accessKey"]
|
||||
if !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
if _, ok = v.(string); !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
|
||||
if globalPolicyOPA == nil {
|
||||
// If OPA is not set and if ldap claim key is set,
|
||||
// allow the claim.
|
||||
if _, ok := claims[ldapUser]; ok {
|
||||
return claims, nil
|
||||
// If OPA is not set and if ldap claim key is set, allow the claim.
|
||||
if _, ok := claims.Lookup(ldapUser); ok {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
// If OPA is not set, session token should
|
||||
// have a policy and its mandatory, reject
|
||||
// requests without policy claim.
|
||||
p, pok := claims[iamPolicyClaimName()]
|
||||
if !pok {
|
||||
_, pokOpenID := claims.Lookup(iamPolicyClaimNameOpenID())
|
||||
_, pokSA := claims.Lookup(iamPolicyClaimNameSA())
|
||||
if !pokOpenID && !pokSA {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
if _, pok = p.(string); !pok {
|
||||
return nil, errAuthentication
|
||||
}
|
||||
sp, spok := claims[iampolicy.SessionPolicyName]
|
||||
// Sub policy is optional, if not set return success.
|
||||
|
||||
sp, spok := claims.Lookup(iampolicy.SessionPolicyName)
|
||||
if !spok {
|
||||
return claims, nil
|
||||
}
|
||||
// Sub policy is set but its not a string, reject such requests
|
||||
spStr, spok := sp.(string)
|
||||
if !spok {
|
||||
return nil, errAuthentication
|
||||
return claims.Map(), nil
|
||||
}
|
||||
// Looks like subpolicy is set and is a string, if set then its
|
||||
// base64 encoded, decode it. Decoding fails reject such requests.
|
||||
spBytes, err := base64.StdEncoding.DecodeString(spStr)
|
||||
spBytes, err := base64.StdEncoding.DecodeString(sp)
|
||||
if err != nil {
|
||||
// Base64 decoding fails, we should log to indicate
|
||||
// something is malforming the request sent by client.
|
||||
logger.LogIf(context.Background(), err, logger.Application)
|
||||
logger.LogIf(r.Context(), err, logger.Application)
|
||||
return nil, errAuthentication
|
||||
}
|
||||
claims[iampolicy.SessionPolicyName] = string(spBytes)
|
||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
||||
}
|
||||
return claims, nil
|
||||
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
// Fetch claims in the security token returned by the client and validate the token.
|
||||
@@ -268,7 +247,7 @@ func checkClaimsFromToken(r *http.Request, cred auth.Credentials) (map[string]in
|
||||
}
|
||||
claims, err := getClaimsFromToken(r)
|
||||
if err != nil {
|
||||
return nil, toAPIErrorCode(context.Background(), err)
|
||||
return nil, toAPIErrorCode(r.Context(), err)
|
||||
}
|
||||
return claims, ErrNone
|
||||
}
|
||||
@@ -293,7 +272,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
||||
var cred auth.Credentials
|
||||
switch getRequestAuthType(r) {
|
||||
case authTypeUnknown, authTypeStreamingSigned:
|
||||
return accessKey, owner, ErrAccessDenied
|
||||
return accessKey, owner, ErrSignatureVersionNotSupported
|
||||
case authTypePresignedV2, authTypeSignedV2:
|
||||
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||
return accessKey, owner, s3Err
|
||||
@@ -355,7 +334,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
||||
// Request is allowed return the appropriate access key.
|
||||
return cred.AccessKey, owner, ErrNone
|
||||
}
|
||||
return accessKey, owner, ErrAccessDenied
|
||||
return cred.AccessKey, owner, ErrAccessDenied
|
||||
}
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
@@ -369,7 +348,7 @@ func checkRequestAuthTypeToAccessKey(ctx context.Context, r *http.Request, actio
|
||||
// Request is allowed return the appropriate access key.
|
||||
return cred.AccessKey, owner, ErrNone
|
||||
}
|
||||
return accessKey, owner, ErrAccessDenied
|
||||
return cred.AccessKey, owner, ErrAccessDenied
|
||||
}
|
||||
|
||||
// Verify if request has valid AWS Signature Version '2'.
|
||||
@@ -402,12 +381,11 @@ func isReqAuthenticated(ctx context.Context, r *http.Request, region string, sty
|
||||
err error
|
||||
contentMD5, contentSHA256 []byte
|
||||
)
|
||||
|
||||
// Extract 'Content-Md5' if present.
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; ok {
|
||||
contentMD5, err = base64.StdEncoding.Strict().DecodeString(r.Header.Get(xhttp.ContentMD5))
|
||||
if err != nil || len(contentMD5) == 0 {
|
||||
return ErrInvalidDigest
|
||||
}
|
||||
contentMD5, err = checkValidMD5(r.Header)
|
||||
if err != nil {
|
||||
return ErrInvalidDigest
|
||||
}
|
||||
|
||||
// Extract either 'X-Amz-Content-Sha256' header or 'X-Amz-Content-Sha256' query parameter (if V4 presigned)
|
||||
@@ -483,7 +461,7 @@ func (a authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
writeErrorResponse(context.Background(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrSignatureVersionNotSupported), r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
|
||||
// isPutActionAllowed - check if PUT operation is allowed on the resource, this
|
||||
@@ -494,7 +472,7 @@ func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.R
|
||||
var owner bool
|
||||
switch atype {
|
||||
case authTypeUnknown:
|
||||
return ErrAccessDenied
|
||||
return ErrSignatureVersionNotSupported
|
||||
case authTypeSignedV2, authTypePresignedV2:
|
||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned:
|
||||
@@ -513,7 +491,7 @@ func isPutActionAllowed(atype authType, bucketName, objectName string, r *http.R
|
||||
if cred.AccessKey == "" {
|
||||
if globalPolicySys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Action: policy.PutObjectAction,
|
||||
Action: policy.Action(action),
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, "", "", nil),
|
||||
IsOwner: false,
|
||||
|
||||
+29
-64
@@ -52,9 +52,21 @@ func (h *healRoutine) queueHealTask(task healTask) {
|
||||
h.tasks <- task
|
||||
}
|
||||
|
||||
func waitForLowHTTPReq(tolerance int32) {
|
||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
||||
// Wait at max 10 minute for an inprogress request before proceeding to heal
|
||||
waitCount := 600
|
||||
// Any requests in progress, delay the heal.
|
||||
for (httpServer.GetRequestCount() >= tolerance) &&
|
||||
waitCount > 0 {
|
||||
waitCount--
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for heal requests and process them
|
||||
func (h *healRoutine) run() {
|
||||
ctx := context.Background()
|
||||
func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
||||
for {
|
||||
select {
|
||||
case task, ok := <-h.tasks:
|
||||
@@ -62,34 +74,26 @@ func (h *healRoutine) run() {
|
||||
break
|
||||
}
|
||||
|
||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
||||
// Wait at max 10 minute for an inprogress request before proceeding to heal
|
||||
waitCount := 600
|
||||
// Any requests in progress, delay the heal.
|
||||
for (httpServer.GetRequestCount() >= int32(globalEndpoints.Nodes())) &&
|
||||
waitCount > 0 {
|
||||
waitCount--
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
// Wait and proceed if there are active requests
|
||||
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()))
|
||||
|
||||
var res madmin.HealResultItem
|
||||
var err error
|
||||
bucket, object := urlPath2BucketObjectName(task.path)
|
||||
bucket, object := path2BucketObject(task.path)
|
||||
switch {
|
||||
case bucket == "" && object == "":
|
||||
res, err = bgHealDiskFormat(ctx, task.opts)
|
||||
res, err = healDiskFormat(ctx, objAPI, task.opts)
|
||||
case bucket != "" && object == "":
|
||||
res, err = bgHealBucket(ctx, bucket, task.opts)
|
||||
res, err = objAPI.HealBucket(ctx, bucket, task.opts.DryRun, task.opts.Remove)
|
||||
case bucket != "" && object != "":
|
||||
res, err = bgHealObject(ctx, bucket, object, task.opts)
|
||||
res, err = objAPI.HealObject(ctx, bucket, object, task.opts)
|
||||
}
|
||||
if task.responseCh != nil {
|
||||
task.responseCh <- healResult{result: res, err: err}
|
||||
}
|
||||
case <-h.doneCh:
|
||||
return
|
||||
case <-GlobalServiceDoneCh:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -103,45 +107,27 @@ func initHealRoutine() *healRoutine {
|
||||
|
||||
}
|
||||
|
||||
func startBackgroundHealing() {
|
||||
ctx := context.Background()
|
||||
|
||||
var objAPI ObjectLayer
|
||||
for {
|
||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
func startBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||
// Run the background healer
|
||||
globalBackgroundHealRoutine = initHealRoutine()
|
||||
go globalBackgroundHealRoutine.run()
|
||||
go globalBackgroundHealRoutine.run(ctx, objAPI)
|
||||
|
||||
// Launch the background healer sequence to track
|
||||
// background healing operations
|
||||
info := objAPI.StorageInfo(ctx)
|
||||
info := objAPI.StorageInfo(ctx, false)
|
||||
numDisks := info.Backend.OnlineDisks.Sum() + info.Backend.OfflineDisks.Sum()
|
||||
nh := newBgHealSequence(numDisks)
|
||||
globalBackgroundHealState.LaunchNewHealSequence(nh)
|
||||
}
|
||||
|
||||
func initBackgroundHealing() {
|
||||
go startBackgroundHealing()
|
||||
func initBackgroundHealing(ctx context.Context, objAPI ObjectLayer) {
|
||||
go startBackgroundHealing(ctx, objAPI)
|
||||
}
|
||||
|
||||
// bgHealDiskFormat - heals format.json, return value indicates if a
|
||||
// healDiskFormat - heals format.json, return value indicates if a
|
||||
// failure error occurred.
|
||||
func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil {
|
||||
return madmin.HealResultItem{}, errServerNotInitialized
|
||||
}
|
||||
|
||||
res, err := objectAPI.HealFormat(ctx, opts.DryRun)
|
||||
func healDiskFormat(ctx context.Context, objAPI ObjectLayer, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||
res, err := objAPI.HealFormat(ctx, opts.DryRun)
|
||||
|
||||
// return any error, ignore error returned when disks have
|
||||
// already healed.
|
||||
@@ -162,24 +148,3 @@ func bgHealDiskFormat(ctx context.Context, opts madmin.HealOpts) (madmin.HealRes
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// bghealBucket - traverses and heals given bucket
|
||||
func bgHealBucket(ctx context.Context, bucket string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil {
|
||||
return madmin.HealResultItem{}, errServerNotInitialized
|
||||
}
|
||||
|
||||
return objectAPI.HealBucket(ctx, bucket, opts.DryRun, opts.Remove)
|
||||
}
|
||||
|
||||
// bgHealObject - heal the given object and record result
|
||||
func bgHealObject(ctx context.Context, bucket, object string, opts madmin.HealOpts) (madmin.HealResultItem, error) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil {
|
||||
return madmin.HealResultItem{}, errServerNotInitialized
|
||||
}
|
||||
return objectAPI.HealObject(ctx, bucket, object, opts.DryRun, opts.Remove, opts.ScanMode)
|
||||
}
|
||||
|
||||
@@ -25,32 +25,19 @@ import (
|
||||
|
||||
const defaultMonitorNewDiskInterval = time.Minute * 10
|
||||
|
||||
func initLocalDisksAutoHeal() {
|
||||
go monitorLocalDisksAndHeal()
|
||||
func initLocalDisksAutoHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
go monitorLocalDisksAndHeal(ctx, objAPI)
|
||||
}
|
||||
|
||||
// monitorLocalDisksAndHeal - ensures that detected new disks are healed
|
||||
// 1. Only the concerned erasure set will be listed and healed
|
||||
// 2. Only the node hosting the disk is responsible to perform the heal
|
||||
func monitorLocalDisksAndHeal() {
|
||||
// Wait until the object layer is ready
|
||||
var objAPI ObjectLayer
|
||||
for {
|
||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
func monitorLocalDisksAndHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
z, ok := objAPI.(*xlZones)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var bgSeq *healSequence
|
||||
var found bool
|
||||
|
||||
@@ -64,64 +51,67 @@ func monitorLocalDisksAndHeal() {
|
||||
|
||||
// Perform automatic disk healing when a disk is replaced locally.
|
||||
for {
|
||||
time.Sleep(defaultMonitorNewDiskInterval)
|
||||
|
||||
// Attempt a heal as the server starts-up first.
|
||||
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
||||
for i, ep := range globalEndpoints {
|
||||
localDisksToHeal := Endpoints{}
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if !endpoint.IsLocal {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(defaultMonitorNewDiskInterval):
|
||||
// Attempt a heal as the server starts-up first.
|
||||
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
||||
for i, ep := range globalEndpoints {
|
||||
localDisksToHeal := Endpoints{}
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if !endpoint.IsLocal {
|
||||
continue
|
||||
}
|
||||
// Try to connect to the current endpoint
|
||||
// and reformat if the current disk is not formatted
|
||||
_, _, err := connectEndpoint(endpoint)
|
||||
if err == errUnformattedDisk {
|
||||
localDisksToHeal = append(localDisksToHeal, endpoint)
|
||||
}
|
||||
}
|
||||
if len(localDisksToHeal) == 0 {
|
||||
continue
|
||||
}
|
||||
// Try to connect to the current endpoint
|
||||
// and reformat if the current disk is not formatted
|
||||
_, _, err := connectEndpoint(endpoint)
|
||||
if err == errUnformattedDisk {
|
||||
localDisksToHeal = append(localDisksToHeal, endpoint)
|
||||
}
|
||||
localDisksInZoneHeal[i] = localDisksToHeal
|
||||
}
|
||||
if len(localDisksToHeal) == 0 {
|
||||
continue
|
||||
}
|
||||
localDisksInZoneHeal[i] = localDisksToHeal
|
||||
}
|
||||
|
||||
// Reformat disks
|
||||
bgSeq.sourceCh <- SlashSeparator
|
||||
// Reformat disks
|
||||
bgSeq.sourceCh <- healSource{path: SlashSeparator}
|
||||
|
||||
// Ensure that reformatting disks is finished
|
||||
bgSeq.sourceCh <- nopHeal
|
||||
// Ensure that reformatting disks is finished
|
||||
bgSeq.sourceCh <- healSource{path: nopHeal}
|
||||
|
||||
var erasureSetInZoneToHeal = make([][]int, len(localDisksInZoneHeal))
|
||||
// Compute the list of erasure set to heal
|
||||
for i, localDisksToHeal := range localDisksInZoneHeal {
|
||||
var erasureSetToHeal []int
|
||||
for _, endpoint := range localDisksToHeal {
|
||||
// Load the new format of this passed endpoint
|
||||
_, format, err := connectEndpoint(endpoint)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
var erasureSetInZoneToHeal = make([][]int, len(localDisksInZoneHeal))
|
||||
// Compute the list of erasure set to heal
|
||||
for i, localDisksToHeal := range localDisksInZoneHeal {
|
||||
var erasureSetToHeal []int
|
||||
for _, endpoint := range localDisksToHeal {
|
||||
// Load the new format of this passed endpoint
|
||||
_, format, err := connectEndpoint(endpoint)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
// Calculate the set index where the current endpoint belongs
|
||||
setIndex, _, err := findDiskIndex(z.zones[i].format, format)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
erasureSetToHeal = append(erasureSetToHeal, setIndex)
|
||||
}
|
||||
// Calculate the set index where the current endpoint belongs
|
||||
setIndex, _, err := findDiskIndex(z.zones[i].format, format)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
erasureSetToHeal = append(erasureSetToHeal, setIndex)
|
||||
erasureSetInZoneToHeal[i] = erasureSetToHeal
|
||||
}
|
||||
erasureSetInZoneToHeal[i] = erasureSetToHeal
|
||||
}
|
||||
|
||||
// Heal all erasure sets that need
|
||||
for i, erasureSetToHeal := range erasureSetInZoneToHeal {
|
||||
for _, setIndex := range erasureSetToHeal {
|
||||
err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
// Heal all erasure sets that need
|
||||
for i, erasureSetToHeal := range erasureSetInZoneToHeal {
|
||||
for _, setIndex := range erasureSetToHeal {
|
||||
err := healErasureSet(ctx, setIndex, z.zones[i].sets[setIndex])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -27,6 +27,14 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
type errHashMismatch struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (err *errHashMismatch) Error() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// Calculates bitrot in chunks and writes the hash into the stream.
|
||||
type streamingBitrotWriter struct {
|
||||
iow *io.PipeWriter
|
||||
@@ -132,8 +140,8 @@ func (b *streamingBitrotReader) ReadAt(buf []byte, offset int64) (int, error) {
|
||||
b.h.Write(buf)
|
||||
|
||||
if !bytes.Equal(b.h.Sum(nil), b.hashBytes) {
|
||||
err = fmt.Errorf("hashes do not match expected %s, got %s",
|
||||
hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))
|
||||
err := &errHashMismatch{fmt.Sprintf("hashes do not match expected %s, got %s",
|
||||
hex.EncodeToString(b.hashBytes), hex.EncodeToString(b.h.Sum(nil)))}
|
||||
logger.LogIf(context.Background(), err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -62,9 +62,9 @@ func (s1 ServerSystemConfig) Diff(s2 ServerSystemConfig) error {
|
||||
return fmt.Errorf("Expected platform '%s', found to be running '%s'",
|
||||
s1.MinioPlatform, s2.MinioPlatform)
|
||||
}
|
||||
if s1.MinioEndpoints.Nodes() != s2.MinioEndpoints.Nodes() {
|
||||
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.Nodes(),
|
||||
s2.MinioEndpoints.Nodes())
|
||||
if s1.MinioEndpoints.NEndpoints() != s2.MinioEndpoints.NEndpoints() {
|
||||
return fmt.Errorf("Expected number of endpoints %d, seen %d", s1.MinioEndpoints.NEndpoints(),
|
||||
s2.MinioEndpoints.NEndpoints())
|
||||
}
|
||||
|
||||
for i, ep := range s1.MinioEndpoints {
|
||||
@@ -110,7 +110,7 @@ func registerBootstrapRESTHandlers(router *mux.Router) {
|
||||
httpTraceHdrs(server.VerifyHandler))
|
||||
}
|
||||
|
||||
// client to talk to bootstrap Nodes.
|
||||
// client to talk to bootstrap NEndpoints.
|
||||
type bootstrapRESTClient struct {
|
||||
endpoint Endpoint
|
||||
restClient *rest.Client
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
// Bucket Encryption configuration file name.
|
||||
bucketSSEConfig = "bucket-encryption.xml"
|
||||
)
|
||||
|
||||
// PutBucketEncryptionHandler - Stores given bucket encryption configuration
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketEncryption.html
|
||||
func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketEncryption")
|
||||
|
||||
defer logger.AuditLog(w, r, "PutBucketEncryption", mustGetClaimsFromToken(r))
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// PutBucketEncyrption API requires Content-Md5
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse bucket encryption xml
|
||||
encConfig, err := validateBucketSSEConfig(io.LimitReader(r.Body, maxBucketSSEConfigSize))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Return error if KMS is not initialized
|
||||
if GlobalKMS == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Store the bucket encryption configuration in the object layer
|
||||
if err = objAPI.SetBucketSSEConfig(ctx, bucket, encConfig); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Update the in-memory bucket encryption cache
|
||||
globalBucketSSEConfigSys.Set(bucket, *encConfig)
|
||||
|
||||
// Update peer MinIO servers of the updated bucket encryption config
|
||||
globalNotificationSys.SetBucketSSEConfig(ctx, bucket, encConfig)
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketEncryptionHandler - Returns bucket policy configuration
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketEncryption.html
|
||||
func (api objectAPIHandlers) GetBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketEncryption")
|
||||
|
||||
defer logger.AuditLog(w, r, "GetBucketEncryption", mustGetClaimsFromToken(r))
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists
|
||||
var err error
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch bucket encryption configuration from object layer
|
||||
var encConfig *bucketsse.BucketSSEConfig
|
||||
if encConfig, err = objAPI.GetBucketSSEConfig(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var encConfigData []byte
|
||||
if encConfigData, err = xml.Marshal(encConfig); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Write bucket encryption configuration to client
|
||||
writeSuccessResponseXML(w, encConfigData)
|
||||
}
|
||||
|
||||
// DeleteBucketEncryptionHandler - Removes bucket encryption configuration
|
||||
func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteBucketEncryption")
|
||||
|
||||
defer logger.AuditLog(w, r, "DeleteBucketEncryption", mustGetClaimsFromToken(r))
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketEncryptionAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists
|
||||
var err error
|
||||
if _, err = objAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete bucket encryption config from object layer
|
||||
if err = objAPI.DeleteBucketSSEConfig(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Remove entry from the in-memory bucket encryption cache
|
||||
globalBucketSSEConfigSys.Remove(bucket)
|
||||
// Update peer MinIO servers of the updated bucket encryption config
|
||||
globalNotificationSys.RemoveBucketSSEConfig(ctx, bucket)
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||
)
|
||||
|
||||
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
||||
type BucketSSEConfigSys struct {
|
||||
sync.RWMutex
|
||||
bucketSSEConfigMap map[string]bucketsse.BucketSSEConfig
|
||||
}
|
||||
|
||||
// NewBucketSSEConfigSys - Creates an empty in-memory bucket encryption configuration cache
|
||||
func NewBucketSSEConfigSys() *BucketSSEConfigSys {
|
||||
return &BucketSSEConfigSys{
|
||||
bucketSSEConfigMap: make(map[string]bucketsse.BucketSSEConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// load - Loads the bucket encryption configuration for the given list of buckets
|
||||
func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
config, err := objAPI.GetBucketSSEConfig(context.Background(), bucket.Name)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
||||
sys.Remove(bucket.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
sys.Set(bucket.Name, *config)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init - Initializes in-memory bucket encryption config cache for the given list of buckets
|
||||
func (sys *BucketSSEConfigSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// We don't cache bucket encryption config in gateway mode, nothing to do.
|
||||
if globalIsGateway {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load bucket encryption config cache once during boot.
|
||||
return sys.load(buckets, objAPI)
|
||||
}
|
||||
|
||||
// Get - gets bucket encryption config for the given bucket.
|
||||
func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSEConfig, ok bool) {
|
||||
// We don't cache bucket encryption config in gateway mode.
|
||||
if globalIsGateway {
|
||||
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := objAPI.GetBucketSSEConfig(context.Background(), bucket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return *cfg, true
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
config, ok = sys.bucketSSEConfigMap[bucket]
|
||||
return
|
||||
}
|
||||
|
||||
// Set - sets bucket encryption config to given bucket name.
|
||||
func (sys *BucketSSEConfigSys) Set(bucket string, config bucketsse.BucketSSEConfig) {
|
||||
// We don't cache bucket encryption config in gateway mode.
|
||||
if globalIsGateway {
|
||||
return
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
sys.bucketSSEConfigMap[bucket] = config
|
||||
}
|
||||
|
||||
// Remove - removes bucket encryption config for given bucket.
|
||||
func (sys *BucketSSEConfigSys) Remove(bucket string) {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
delete(sys.bucketSSEConfigMap, bucket)
|
||||
}
|
||||
|
||||
// saveBucketSSEConfig - save bucket encryption config for given bucket.
|
||||
func saveBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string, config *bucketsse.BucketSSEConfig) error {
|
||||
data, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Path to store bucket encryption config for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
||||
return saveConfig(ctx, objAPI, configFile, data)
|
||||
}
|
||||
|
||||
// getBucketSSEConfig - get bucket encryption config for given bucket.
|
||||
func getBucketSSEConfig(objAPI ObjectLayer, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||
// Path to bucket-encryption.xml for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
||||
configData, err := readConfig(context.Background(), objAPI, configFile)
|
||||
if err != nil {
|
||||
if err == errConfigNotFound {
|
||||
err = BucketSSEConfigNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bucketsse.ParseBucketSSEConfig(bytes.NewReader(configData))
|
||||
}
|
||||
|
||||
// removeBucketSSEConfig - removes bucket encryption config for given bucket.
|
||||
func removeBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket string) error {
|
||||
// Path to bucket-encryption.xml for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketSSEConfig)
|
||||
|
||||
if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
return BucketSSEConfigNotFound{Bucket: bucket}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBucketSSEConfig parses bucket encryption configuration and validates if it is supported by MinIO.
|
||||
func validateBucketSSEConfig(r io.Reader) (*bucketsse.BucketSSEConfig, error) {
|
||||
encConfig, err := bucketsse.ParseBucketSSEConfig(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(encConfig.Rules) == 1 && encConfig.Rules[0].DefaultEncryptionAction.Algorithm == bucketsse.AES256 {
|
||||
return encConfig, nil
|
||||
}
|
||||
return nil, errors.New("Unsupported bucket encryption configuration")
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateBucketSSEConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
inputXML string
|
||||
expectedErr error
|
||||
shouldPass bool
|
||||
}{
|
||||
// MinIO supported XML
|
||||
{
|
||||
inputXML: `<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<Rule>
|
||||
<ApplyServerSideEncryptionByDefault>
|
||||
<SSEAlgorithm>AES256</SSEAlgorithm>
|
||||
</ApplyServerSideEncryptionByDefault>
|
||||
</Rule>
|
||||
</ServerSideEncryptionConfiguration>`,
|
||||
expectedErr: nil,
|
||||
shouldPass: true,
|
||||
},
|
||||
// Unsupported XML
|
||||
{
|
||||
inputXML: `<ServerSideEncryptionConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<Rule>
|
||||
<ApplyServerSideEncryptionByDefault>
|
||||
<SSEAlgorithm>aws:kms</SSEAlgorithm>
|
||||
<KMSMasterKeyID>arn:aws:kms:us-east-1:1234/5678example</KMSMasterKeyID>
|
||||
</ApplyServerSideEncryptionByDefault>
|
||||
</Rule>
|
||||
</ServerSideEncryptionConfiguration>`,
|
||||
expectedErr: errors.New("Unsupported bucket encryption configuration"),
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
_, err := validateBucketSSEConfig(bytes.NewReader([]byte(tc.inputXML)))
|
||||
if tc.shouldPass && err != nil {
|
||||
t.Fatalf("Test case %d: Expected to succeed but got %s", i+1, err)
|
||||
}
|
||||
|
||||
if !tc.shouldPass {
|
||||
if err == nil || err != nil && err.Error() != tc.expectedErr.Error() {
|
||||
t.Fatalf("Test case %d: Expected %s but got %s", i+1, tc.expectedErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
// Validate all the ListObjects query arguments, returns an APIErrorCode
|
||||
|
||||
+135
-124
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2015, 2016, 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2015-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -36,11 +35,12 @@ import (
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
)
|
||||
|
||||
@@ -74,40 +74,45 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
||||
return
|
||||
}
|
||||
|
||||
bucketSet := set.NewStringSet()
|
||||
bucketsSet := set.NewStringSet()
|
||||
bucketsToBeUpdated := set.NewStringSet()
|
||||
bucketsInConflict := set.NewStringSet()
|
||||
for _, bucket := range buckets {
|
||||
bucketsSet.Add(bucket.Name)
|
||||
r, ok := dnsBuckets[bucket.Name]
|
||||
if !ok {
|
||||
bucketsToBeUpdated.Add(bucket.Name)
|
||||
continue
|
||||
}
|
||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||
if globalDomainIPs.Difference(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||
// No difference in terms of domainIPs and nothing
|
||||
// has changed so we don't change anything on the etcd.
|
||||
continue
|
||||
}
|
||||
// if domain IPs intersect then it won't be an empty set.
|
||||
// such an intersection means that bucket exists on etcd.
|
||||
// but if we do see a difference with local domain IPs with
|
||||
// hostSlice from etcd then we should update with newer
|
||||
// domainIPs, we proceed to do that here.
|
||||
bucketsToBeUpdated.Add(bucket.Name)
|
||||
continue
|
||||
}
|
||||
// No IPs seem to intersect, this means that bucket exists but has
|
||||
// different IP addresses perhaps from a different deployment.
|
||||
// bucket names are globally unique in federation at a given
|
||||
// path prefix, name collision is not allowed. We simply log
|
||||
// an error and continue.
|
||||
bucketsInConflict.Add(bucket.Name)
|
||||
}
|
||||
|
||||
// Add buckets that are not registered with the DNS
|
||||
// Add/update buckets that are not registered with the DNS
|
||||
g := errgroup.WithNErrs(len(buckets))
|
||||
for index := range buckets {
|
||||
bucketSet.Add(buckets[index].Name)
|
||||
bucketsToBeUpdatedSlice := bucketsToBeUpdated.ToSlice()
|
||||
for index := range bucketsToBeUpdatedSlice {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
r, gerr := globalDNSConfig.Get(buckets[index].Name)
|
||||
if gerr != nil {
|
||||
if gerr == dns.ErrNoEntriesFound {
|
||||
return globalDNSConfig.Put(buckets[index].Name)
|
||||
}
|
||||
return gerr
|
||||
}
|
||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||
if globalDomainIPs.Difference(set.CreateStringSet(getHostsSlice(r)...)).IsEmpty() {
|
||||
// No difference in terms of domainIPs and nothing
|
||||
// has changed so we don't change anything on the etcd.
|
||||
return nil
|
||||
}
|
||||
// if domain IPs intersect then it won't be an empty set.
|
||||
// such an intersection means that bucket exists on etcd.
|
||||
// but if we do see a difference with local domain IPs with
|
||||
// hostSlice from etcd then we should update with newer
|
||||
// domainIPs, we proceed to do that here.
|
||||
return globalDNSConfig.Put(buckets[index].Name)
|
||||
}
|
||||
// No IPs seem to intersect, this means that bucket exists but has
|
||||
// different IP addresses perhaps from a different deployment.
|
||||
// bucket names are globally unique in federation at a given
|
||||
// path prefix, name collision is not allowed. We simply log
|
||||
// an error and continue.
|
||||
return fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", buckets[index].Name, globalDomainIPs.ToSlice())
|
||||
return globalDNSConfig.Put(bucketsToBeUpdatedSlice[index])
|
||||
}, index)
|
||||
}
|
||||
|
||||
@@ -117,36 +122,26 @@ func initFederatorBackend(buckets []BucketInfo, objLayer ObjectLayer) {
|
||||
}
|
||||
}
|
||||
|
||||
g = errgroup.WithNErrs(len(dnsBuckets))
|
||||
// Remove buckets that are in DNS for this server, but aren't local
|
||||
for index := range dnsBuckets {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
// This is a local bucket that exists, so we can continue
|
||||
if bucketSet.Contains(dnsBuckets[index].Key) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is not for our server, so we can continue
|
||||
hostPort := net.JoinHostPort(dnsBuckets[index].Host, string(dnsBuckets[index].Port))
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(hostPort)).IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We go to here, so we know the bucket no longer exists,
|
||||
// but is registered in DNS to this server
|
||||
if err := globalDNSConfig.DeleteRecord(dnsBuckets[index]); err != nil {
|
||||
return fmt.Errorf("Failed to remove DNS entry for %s due to %w",
|
||||
dnsBuckets[index].Key, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, index)
|
||||
for _, bucket := range bucketsInConflict.ToSlice() {
|
||||
logger.LogIf(context.Background(), fmt.Errorf("Unable to add bucket DNS entry for bucket %s, an entry exists for the same bucket. Use one of these IP addresses %v to access the bucket", bucket, globalDomainIPs.ToSlice()))
|
||||
}
|
||||
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
// Remove buckets that are in DNS for this server, but aren't local
|
||||
for bucket, records := range dnsBuckets {
|
||||
if bucketsSet.Contains(bucket) {
|
||||
continue
|
||||
}
|
||||
|
||||
if globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(records)...)).IsEmpty() {
|
||||
// This is not for our server, so we can continue
|
||||
continue
|
||||
}
|
||||
|
||||
// We go to here, so we know the bucket no longer exists,
|
||||
// but is registered in DNS to this server
|
||||
if err = globalDNSConfig.Delete(bucket); err != nil {
|
||||
logger.LogIf(context.Background(), fmt.Errorf("Failed to remove DNS entry for %s due to %w",
|
||||
bucket, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,7 +266,7 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
listBuckets := objectAPI.ListBuckets
|
||||
|
||||
accessKey, owner, s3Error := checkRequestAuthTypeToAccessKey(ctx, r, policy.ListAllMyBucketsAction, "", "")
|
||||
if s3Error != ErrNone {
|
||||
if s3Error != ErrNone && s3Error != ErrAccessDenied {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -284,16 +279,11 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
bucketSet := set.NewStringSet()
|
||||
for _, dnsRecord := range dnsBuckets {
|
||||
if bucketSet.Contains(dnsRecord.Key) {
|
||||
continue
|
||||
}
|
||||
for _, dnsRecords := range dnsBuckets {
|
||||
bucketsInfo = append(bucketsInfo, BucketInfo{
|
||||
Name: dnsRecord.Key,
|
||||
Created: dnsRecord.CreationDate,
|
||||
Name: dnsRecords[0].Key,
|
||||
Created: dnsRecords[0].CreationDate,
|
||||
})
|
||||
bucketSet.Add(dnsRecord.Key)
|
||||
}
|
||||
} else {
|
||||
// Invoke the list buckets.
|
||||
@@ -305,32 +295,43 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
}
|
||||
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", "")
|
||||
if s3Error == ErrAccessDenied {
|
||||
// Set prefix value for "s3:prefix" policy conditionals.
|
||||
r.Header.Set("prefix", "")
|
||||
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", SlashSeparator)
|
||||
// Set delimiter value for "s3:delimiter" policy conditionals.
|
||||
r.Header.Set("delimiter", SlashSeparator)
|
||||
|
||||
// err will be nil here as we already called this function
|
||||
// earlier in this request.
|
||||
claims, _ := getClaimsFromToken(r)
|
||||
var newBucketsInfo []BucketInfo
|
||||
for _, bucketInfo := range bucketsInfo {
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: bucketInfo.Name,
|
||||
ConditionValues: getConditionValues(r, "", accessKey, claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
newBucketsInfo = append(newBucketsInfo, bucketInfo)
|
||||
// err will be nil here as we already called this function
|
||||
// earlier in this request.
|
||||
claims, _ := getClaimsFromToken(r)
|
||||
n := 0
|
||||
// Use the following trick to filter in place
|
||||
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
|
||||
for _, bucketInfo := range bucketsInfo {
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: accessKey,
|
||||
Action: iampolicy.ListBucketAction,
|
||||
BucketName: bucketInfo.Name,
|
||||
ConditionValues: getConditionValues(r, "", accessKey, claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
bucketsInfo[n] = bucketInfo
|
||||
n++
|
||||
}
|
||||
}
|
||||
bucketsInfo = bucketsInfo[:n]
|
||||
// No buckets can be filtered return access denied error.
|
||||
if len(bucketsInfo) == 0 {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate response.
|
||||
response := generateListBucketsResponse(newBucketsInfo)
|
||||
response := generateListBucketsResponse(bucketsInfo)
|
||||
encodedSuccessResponse := encodeResponse(response)
|
||||
|
||||
// Write response.
|
||||
@@ -352,6 +353,13 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Md5 is requied should be set
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if r.ContentLength <= 0 {
|
||||
@@ -359,34 +367,14 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Md5 is requied should be set
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Allocate incoming content length bytes.
|
||||
var deleteXMLBytes []byte
|
||||
const maxBodySize = 2 * 1000 * 1024 // The max. XML contains 1000 object names (each at most 1024 bytes long) + XML overhead
|
||||
if r.ContentLength > maxBodySize { // Only allocated memory for at most 1000 objects
|
||||
deleteXMLBytes = make([]byte, maxBodySize)
|
||||
} else {
|
||||
deleteXMLBytes = make([]byte, r.ContentLength)
|
||||
}
|
||||
|
||||
// Read incoming body XML bytes.
|
||||
if _, err := io.ReadFull(r.Body, deleteXMLBytes); err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponse(ctx, w, toAdminAPIErr(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// The max. XML contains 100000 object names (each at most 1024 bytes long) + XML overhead
|
||||
const maxBodySize = 2 * 100000 * 1024
|
||||
|
||||
// Unmarshal list of keys to be deleted.
|
||||
deleteObjects := &DeleteObjectsRequest{}
|
||||
if err := xml.Unmarshal(deleteXMLBytes, deleteObjects); err != nil {
|
||||
if err := xmlDecoder(r.Body, deleteObjects, maxBodySize); err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -533,7 +521,8 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if globalDNSConfig != nil {
|
||||
if _, err := globalDNSConfig.Get(bucket); err != nil {
|
||||
sr, err := globalDNSConfig.Get(bucket)
|
||||
if err != nil {
|
||||
if err == dns.ErrNoEntriesFound {
|
||||
// Proceed to creating a bucket.
|
||||
if err = objectAPI.MakeBucketWithLocation(ctx, bucket, location); err != nil {
|
||||
@@ -550,7 +539,7 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||
objectAPI.DeleteBucket(ctx, bucket)
|
||||
objectAPI.DeleteBucket(ctx, bucket, false)
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -566,7 +555,15 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
|
||||
}
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrBucketAlreadyOwnedByYou), r.URL, guessIsBrowserReq(r))
|
||||
apiErr := ErrBucketAlreadyExists
|
||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||
apiErr = ErrBucketAlreadyOwnedByYou
|
||||
}
|
||||
// No IPs seem to intersect, this means that bucket exists but has
|
||||
// different IP addresses perhaps from a different deployment.
|
||||
// bucket names are globally unique in federation at a given
|
||||
// path prefix, name collision is not allowed. Return appropriate error.
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(apiErr), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -577,14 +574,14 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if objectLockEnabled {
|
||||
if objectLockEnabled && !globalIsGateway {
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
globalBucketObjectLockConfig.Set(bucket, Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, Retention{})
|
||||
globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
|
||||
}
|
||||
|
||||
// Make sure to add Location information here only for bucket
|
||||
@@ -758,8 +755,10 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
var objectEncryptionKey []byte
|
||||
|
||||
// Check if bucket encryption is enabled
|
||||
_, encEnabled := globalBucketSSEConfigSys.Get(bucket)
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
if globalAutoEncryption && !crypto.SSEC.IsRequested(r.Header) {
|
||||
if (globalAutoEncryption || encEnabled) && !crypto.SSEC.IsRequested(r.Header) {
|
||||
r.Header.Add(crypto.SSEHeader, crypto.SSEAlgorithmAES256)
|
||||
}
|
||||
// get gateway encryption options
|
||||
@@ -889,6 +888,18 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
forceDelete := false
|
||||
if vs, found := r.Header[http.CanonicalHeaderKey("x-minio-force-delete")]; found {
|
||||
switch strings.ToLower(strings.Join(vs, "")) {
|
||||
case "true":
|
||||
forceDelete = true
|
||||
case "false":
|
||||
default:
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
@@ -903,7 +914,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
deleteBucket := objectAPI.DeleteBucket
|
||||
|
||||
// Attempt to delete bucket.
|
||||
if err := deleteBucket(ctx, bucket); err != nil {
|
||||
if err := deleteBucket(ctx, bucket, forceDelete); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -1005,7 +1016,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
config, err := parseObjectLockConfig(r.Body)
|
||||
config, err := objectlock.ParseObjectLockConfig(r.Body)
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
@@ -1099,7 +1110,7 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
return
|
||||
}
|
||||
|
||||
if configData, err = xml.Marshal(newObjectLockConfig()); err != nil {
|
||||
if configData, err = xml.Marshal(objectlock.NewObjectLockConfig()); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,6 +29,51 @@ import (
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Wrapper for calling RemoveBucket HTTP handler tests for both XL multiple disks and single node setup.
|
||||
func TestRemoveBucketHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testRemoveBucketHandler, []string{"RemoveBucket"})
|
||||
}
|
||||
|
||||
func testRemoveBucketHandler(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
_, err := obj.PutObject(context.Background(), bucketName, "test-object", mustGetPutObjReader(t, bytes.NewBuffer([]byte{}), int64(0), "", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), ObjectOptions{})
|
||||
// if object upload fails stop the test.
|
||||
if err != nil {
|
||||
t.Fatalf("Error uploading object: <ERROR> %v", err)
|
||||
}
|
||||
|
||||
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request for DELETE bucket.
|
||||
req, err := newTestSignedRequestV4("DELETE", getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: <ERROR> %v", instanceType, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
switch rec.Code {
|
||||
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
|
||||
t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, rec.Code)
|
||||
}
|
||||
|
||||
// Verify response of the V2 signed HTTP request.
|
||||
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
|
||||
recV2 := httptest.NewRecorder()
|
||||
// construct HTTP request for DELETE bucket.
|
||||
reqV2, err := newTestSignedRequestV2("DELETE", getBucketLocationURL("", bucketName), 0, nil, credentials.AccessKey, credentials.SecretKey, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %s: Failed to create HTTP request for RemoveBucketHandler: <ERROR> %v", instanceType, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(recV2, reqV2)
|
||||
switch recV2.Code {
|
||||
case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNoContent:
|
||||
t.Fatalf("Test %v: expected failure, but succeeded with %v", instanceType, recV2.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper for calling GetBucketPolicy HTTP handler tests for both XL multiple disks and single node setup.
|
||||
func TestGetBucketLocationHandler(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testGetBucketLocationHandler, []string{"GetBucketLocation"})
|
||||
|
||||
@@ -22,9 +22,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,6 +49,12 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// PutBucketLifecycle always needs a Content-Md5
|
||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketLifecycleAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -59,15 +66,9 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// PutBucketLifecycle always needs a Content-Md5
|
||||
if _, ok := r.Header["Content-Md5"]; !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML), r.URL, guessIsBrowserReq(r))
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
)
|
||||
|
||||
// Test S3 Bucket lifecycle APIs with wrong credentials
|
||||
func TestBucketLifecycleWrongCredentials(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testBucketLifecycleHandlersWrongCredentials, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
||||
}
|
||||
|
||||
// Test for authentication
|
||||
func testBucketLifecycleHandlersWrongCredentials(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
credentials auth.Credentials, t *testing.T) {
|
||||
// test cases with sample input and expected output.
|
||||
testCases := []struct {
|
||||
method string
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// Sent body
|
||||
body []byte
|
||||
// Expected response
|
||||
expectedRespStatus int
|
||||
lifecycleResponse []byte
|
||||
errorResponse APIErrorResponse
|
||||
shouldPass bool
|
||||
}{
|
||||
// GET empty credentials
|
||||
{
|
||||
method: "GET", bucketName: bucketName,
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "AccessDenied",
|
||||
Message: "Access Denied.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
// GET wrong credentials
|
||||
{
|
||||
method: "GET", bucketName: bucketName,
|
||||
accessKey: "abcd",
|
||||
secretKey: "abcd",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidAccessKeyId",
|
||||
Message: "The access key ID you provided does not exist in our records.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
// PUT empty credentials
|
||||
{
|
||||
method: "PUT",
|
||||
bucketName: bucketName,
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "AccessDenied",
|
||||
Message: "Access Denied.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
// PUT wrong credentials
|
||||
{
|
||||
method: "PUT",
|
||||
bucketName: bucketName,
|
||||
accessKey: "abcd",
|
||||
secretKey: "abcd",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidAccessKeyId",
|
||||
Message: "The access key ID you provided does not exist in our records.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
// DELETE empty credentials
|
||||
{
|
||||
method: "DELETE",
|
||||
bucketName: bucketName,
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "AccessDenied",
|
||||
Message: "Access Denied.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
// DELETE wrong credentials
|
||||
{
|
||||
method: "DELETE",
|
||||
bucketName: bucketName,
|
||||
accessKey: "abcd",
|
||||
secretKey: "abcd",
|
||||
expectedRespStatus: http.StatusForbidden,
|
||||
lifecycleResponse: []byte(""),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidAccessKeyId",
|
||||
Message: "The access key ID you provided does not exist in our records.",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
|
||||
}
|
||||
|
||||
// Test S3 Bucket lifecycle APIs
|
||||
func TestBucketLifecycle(t *testing.T) {
|
||||
ExecObjectLayerAPITest(t, testBucketLifecycleHandlers, []string{"GetBucketLifecycle", "PutBucketLifecycle", "DeleteBucketLifecycle"})
|
||||
}
|
||||
|
||||
// Simple tests of bucket lifecycle: PUT, GET, DELETE.
|
||||
// Tests are related and the order is important.
|
||||
func testBucketLifecycleHandlers(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
creds auth.Credentials, t *testing.T) {
|
||||
|
||||
// test cases with sample input and expected output.
|
||||
testCases := []struct {
|
||||
method string
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
// Sent body
|
||||
body []byte
|
||||
// Expected response
|
||||
expectedRespStatus int
|
||||
lifecycleResponse []byte
|
||||
errorResponse APIErrorResponse
|
||||
shouldPass bool
|
||||
}{
|
||||
// Test case - 1.
|
||||
// Filter contains more than (Prefix,Tag,And) rule
|
||||
{
|
||||
method: "PUT",
|
||||
bucketName: bucketName,
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
body: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidRequest",
|
||||
Message: "Filter must have exactly one of Prefix, Tag, or And specified",
|
||||
},
|
||||
|
||||
shouldPass: false,
|
||||
},
|
||||
// Date contains wrong format
|
||||
{
|
||||
method: "PUT",
|
||||
bucketName: bucketName,
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
body: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix><Tag><Key>Key1</Key><Value>Value1</Value></Tag></Filter><Status>Enabled</Status><Expiration><Date>365</Date></Expiration></Rule></LifecycleConfiguration>`),
|
||||
expectedRespStatus: http.StatusBadRequest,
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "InvalidRequest",
|
||||
Message: "Date must be provided in ISO 8601 format",
|
||||
},
|
||||
|
||||
shouldPass: false,
|
||||
},
|
||||
{
|
||||
method: "PUT",
|
||||
bucketName: bucketName,
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
body: []byte(`<?xml version="1.0" encoding="UTF-8"?><LifecycleConfiguration><Rule><ID>id</ID><Filter><Prefix>logs/</Prefix></Filter><Status>Enabled</Status><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||
expectedRespStatus: http.StatusOK,
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{},
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
bucketName: bucketName,
|
||||
body: []byte(``),
|
||||
expectedRespStatus: http.StatusOK,
|
||||
lifecycleResponse: []byte(`<LifecycleConfiguration><Rule><ID>id</ID><Status>Enabled</Status><Filter><Prefix>logs/</Prefix></Filter><Expiration><Days>365</Days></Expiration></Rule></LifecycleConfiguration>`),
|
||||
errorResponse: APIErrorResponse{},
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
bucketName: bucketName,
|
||||
body: []byte(``),
|
||||
expectedRespStatus: http.StatusNoContent,
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{},
|
||||
shouldPass: true,
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
accessKey: creds.AccessKey,
|
||||
secretKey: creds.SecretKey,
|
||||
bucketName: bucketName,
|
||||
body: []byte(``),
|
||||
expectedRespStatus: http.StatusNotFound,
|
||||
lifecycleResponse: []byte(``),
|
||||
errorResponse: APIErrorResponse{
|
||||
Resource: SlashSeparator + bucketName + SlashSeparator,
|
||||
Code: "NoSuchLifecycleConfiguration",
|
||||
Message: "The lifecycle configuration does not exist",
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
testBucketLifecycle(obj, instanceType, bucketName, apiRouter, t, testCases)
|
||||
}
|
||||
|
||||
// testBucketLifecycle is a generic testing of lifecycle requests
|
||||
func testBucketLifecycle(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler,
|
||||
t *testing.T, testCases []struct {
|
||||
method string
|
||||
bucketName string
|
||||
accessKey string
|
||||
secretKey string
|
||||
body []byte
|
||||
expectedRespStatus int
|
||||
lifecycleResponse []byte
|
||||
errorResponse APIErrorResponse
|
||||
shouldPass bool
|
||||
}) {
|
||||
|
||||
for i, testCase := range testCases {
|
||||
// initialize httptest Recorder, this records any mutations to response writer inside the handler.
|
||||
rec := httptest.NewRecorder()
|
||||
// construct HTTP request
|
||||
req, err := newTestSignedRequestV4(testCase.method, getBucketLifecycleURL("", testCase.bucketName),
|
||||
int64(len(testCase.body)), bytes.NewReader(testCase.body), testCase.accessKey, testCase.secretKey, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: %s: Failed to create HTTP request for GetBucketLocationHandler: <ERROR> %v", i+1, instanceType, err)
|
||||
}
|
||||
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
|
||||
// Call the ServeHTTP to execute the handler.
|
||||
apiRouter.ServeHTTP(rec, req)
|
||||
if rec.Code != testCase.expectedRespStatus {
|
||||
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, rec.Code)
|
||||
}
|
||||
if testCase.shouldPass && !bytes.Equal(testCase.lifecycleResponse, rec.Body.Bytes()) {
|
||||
t.Errorf("Test %d: %s: Expected the response to be `%s`, but instead found `%s`", i+1, instanceType, string(testCase.lifecycleResponse), rec.Body.String())
|
||||
}
|
||||
errorResponse := APIErrorResponse{}
|
||||
err = xml.Unmarshal(rec.Body.Bytes(), &errorResponse)
|
||||
if err != nil && !testCase.shouldPass {
|
||||
t.Fatalf("Test %d: %s: Unable to marshal response body %s", i+1, instanceType, rec.Body.String())
|
||||
}
|
||||
if errorResponse.Resource != testCase.errorResponse.Resource {
|
||||
t.Errorf("Test %d: %s: Expected the error resource to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Resource, errorResponse.Resource)
|
||||
}
|
||||
if errorResponse.Message != testCase.errorResponse.Message {
|
||||
t.Errorf("Test %d: %s: Expected the error message to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Message, errorResponse.Message)
|
||||
}
|
||||
if errorResponse.Code != testCase.errorResponse.Code {
|
||||
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -84,8 +84,8 @@ func (api objectAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter,
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
|
||||
config.SetRegion(globalServerRegion)
|
||||
config.XMLNS = "http://s3.amazonaws.com/doc/2006-03-01/"
|
||||
notificationBytes, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -29,8 +29,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
)
|
||||
|
||||
func getAnonReadOnlyBucketPolicy(bucketName string) *policy.Policy {
|
||||
|
||||
@@ -17,9 +17,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,6 +37,17 @@ import (
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterError(config.FmtError)
|
||||
|
||||
// Initialize globalConsoleSys system
|
||||
globalConsoleSys = NewConsoleLogger(context.Background())
|
||||
logger.AddTarget(globalConsoleSys)
|
||||
|
||||
gob.Register(StorageErr(""))
|
||||
}
|
||||
|
||||
func verifyObjectLayerFeatures(name string, objAPI ObjectLayer) {
|
||||
if (globalAutoEncryption || GlobalKMS != nil) && !objAPI.IsEncryptionSupported() {
|
||||
logger.Fatal(errInvalidArgument,
|
||||
@@ -213,10 +227,22 @@ func handleCommonEnvVars() {
|
||||
globalConfigEncrypted = true
|
||||
}
|
||||
|
||||
if env.IsSet(config.EnvAccessKeyOld) && env.IsSet(config.EnvSecretKeyOld) {
|
||||
oldCred, err := auth.CreateCredentials(env.Get(config.EnvAccessKeyOld, ""), env.Get(config.EnvSecretKeyOld, ""))
|
||||
if err != nil {
|
||||
logger.Fatal(config.ErrInvalidCredentials(err),
|
||||
"Unable to validate the old credentials inherited from the shell environment")
|
||||
}
|
||||
globalOldCred = oldCred
|
||||
os.Unsetenv(config.EnvAccessKeyOld)
|
||||
os.Unsetenv(config.EnvSecretKeyOld)
|
||||
}
|
||||
|
||||
globalWORMEnabled, err = config.LookupWorm()
|
||||
if err != nil {
|
||||
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func logStartupMessage(msg string) {
|
||||
|
||||
+59
-16
@@ -26,7 +26,6 @@ import (
|
||||
"github.com/minio/minio/cmd/config/cache"
|
||||
"github.com/minio/minio/cmd/config/compress"
|
||||
"github.com/minio/minio/cmd/config/etcd"
|
||||
xetcd "github.com/minio/minio/cmd/config/etcd"
|
||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||
xldap "github.com/minio/minio/cmd/config/identity/ldap"
|
||||
"github.com/minio/minio/cmd/config/identity/openid"
|
||||
@@ -38,6 +37,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/target/http"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
func initHelp() {
|
||||
@@ -251,7 +251,7 @@ func validateConfig(s config.Config) error {
|
||||
}
|
||||
}
|
||||
{
|
||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewCustomHTTPTransport())
|
||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -269,17 +269,27 @@ func validateConfig(s config.Config) error {
|
||||
}
|
||||
|
||||
if _, err := openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
||||
NewCustomHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||
globalRootCAs); err != nil {
|
||||
return err
|
||||
{
|
||||
cfg, err := xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||
globalRootCAs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Enabled {
|
||||
conn, cerr := cfg.Connect()
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
NewCustomHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -287,7 +297,7 @@ func validateConfig(s config.Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return notify.TestNotificationTargets(s, GlobalServiceDoneCh, NewCustomHTTPTransport(),
|
||||
return notify.TestNotificationTargets(s, GlobalServiceDoneCh, NewGatewayHTTPTransport(),
|
||||
globalNotificationSys.ConfiguredTargetIDs())
|
||||
}
|
||||
|
||||
@@ -303,13 +313,13 @@ func lookupConfigs(s config.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
etcdCfg, err := xetcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
|
||||
etcdCfg, err := etcd.LookupConfig(s[config.EtcdSubSys][config.Default], globalRootCAs)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||
}
|
||||
|
||||
if etcdCfg.Enabled {
|
||||
globalEtcdClient, err = xetcd.New(etcdCfg)
|
||||
globalEtcdClient, err = etcd.New(etcdCfg)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize etcd config: %w", err))
|
||||
}
|
||||
@@ -362,7 +372,7 @@ func lookupConfigs(s config.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewCustomHTTPTransport())
|
||||
kmsCfg, err := crypto.LookupConfig(s, globalCertsCADir.Get(), NewGatewayHTTPTransport())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to setup KMS config: %w", err))
|
||||
}
|
||||
@@ -381,13 +391,13 @@ func lookupConfigs(s config.Config) {
|
||||
}
|
||||
|
||||
globalOpenIDConfig, err = openid.LookupConfig(s[config.IdentityOpenIDSubSys][config.Default],
|
||||
NewCustomHTTPTransport(), xhttp.DrainBody)
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||
}
|
||||
|
||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
NewCustomHTTPTransport(), xhttp.DrainBody)
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
||||
}
|
||||
@@ -412,18 +422,37 @@ func lookupConfigs(s config.Config) {
|
||||
for _, l := range loggerCfg.HTTP {
|
||||
if l.Enabled {
|
||||
// Enable http logging
|
||||
logger.AddTarget(http.New(l.Endpoint, loggerUserAgent, string(logger.All), NewCustomHTTPTransport()))
|
||||
logger.AddTarget(
|
||||
http.New(http.WithEndpoint(l.Endpoint),
|
||||
http.WithAuthToken(l.AuthToken),
|
||||
http.WithUserAgent(loggerUserAgent),
|
||||
http.WithLogKind(string(logger.All)),
|
||||
http.WithTransport(NewGatewayHTTPTransport()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range loggerCfg.Audit {
|
||||
if l.Enabled {
|
||||
// Enable http audit logging
|
||||
logger.AddAuditTarget(http.New(l.Endpoint, loggerUserAgent, string(logger.All), NewCustomHTTPTransport()))
|
||||
logger.AddAuditTarget(
|
||||
http.New(http.WithEndpoint(l.Endpoint),
|
||||
http.WithAuthToken(l.AuthToken),
|
||||
http.WithUserAgent(loggerUserAgent),
|
||||
http.WithLogKind(string(logger.All)),
|
||||
http.WithTransport(NewGatewayHTTPTransport()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
globalConfigTargetList, err = notify.GetNotificationTargets(s, GlobalServiceDoneCh, NewCustomHTTPTransport())
|
||||
globalConfigTargetList, err = notify.GetNotificationTargets(s, GlobalServiceDoneCh, NewGatewayHTTPTransport())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||
}
|
||||
|
||||
globalEnvTargetList, err = notify.GetNotificationTargets(newServerConfig(), GlobalServiceDoneCh, NewGatewayHTTPTransport())
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize notification target(s): %w", err))
|
||||
}
|
||||
@@ -469,6 +498,20 @@ func GetHelp(subSys, key string, envOnly bool) (Help, error) {
|
||||
|
||||
envHelp := config.HelpKVS{}
|
||||
if envOnly {
|
||||
// Only for multiple targets, make sure
|
||||
// to list the ENV, for regular k/v EnableKey is
|
||||
// implicit, for ENVs we cannot make it implicit.
|
||||
if subSysHelp.MultipleTargets {
|
||||
envK := config.EnvPrefix + strings.Join([]string{
|
||||
strings.ToTitle(subSys), strings.ToTitle(madmin.EnableKey),
|
||||
}, config.EnvWordDelimiter)
|
||||
envHelp = append(envHelp, config.HelpKV{
|
||||
Key: envK,
|
||||
Description: fmt.Sprintf("enable %s target, default is 'off'", subSys),
|
||||
Optional: false,
|
||||
Type: "on|off",
|
||||
})
|
||||
}
|
||||
for _, hkv := range h {
|
||||
envK := config.EnvPrefix + strings.Join([]string{
|
||||
strings.ToTitle(subSys), strings.ToTitle(hkv.Key),
|
||||
|
||||
+18
-39
@@ -21,15 +21,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
@@ -50,12 +47,18 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
||||
// of the object layer.
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
var stop bool
|
||||
|
||||
rquorum := InsufficientReadQuorum{}
|
||||
wquorum := InsufficientWriteQuorum{}
|
||||
bucketNotFound := BucketNotFound{}
|
||||
|
||||
for !stop {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
if encrypted, err = checkBackendEncrypted(objAPI); err != nil {
|
||||
if err == errDiskNotFound ||
|
||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) {
|
||||
if errors.Is(err, errDiskNotFound) ||
|
||||
errors.As(err, &rquorum) ||
|
||||
errors.As(err, &bucketNotFound) {
|
||||
logger.Info("Waiting for config backend to be encrypted..")
|
||||
continue
|
||||
}
|
||||
@@ -88,11 +91,6 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
activeCredOld, err := getOldCreds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doneCh = make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
@@ -106,10 +104,11 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
// Migrate IAM configuration
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI, activeCredOld, encrypted); err != nil {
|
||||
if err == errDiskNotFound ||
|
||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI, globalOldCred, encrypted); err != nil {
|
||||
if errors.Is(err, errDiskNotFound) ||
|
||||
errors.As(err, &rquorum) ||
|
||||
errors.As(err, &wquorum) ||
|
||||
errors.As(err, &bucketNotFound) {
|
||||
logger.Info("Waiting for config backend to be encrypted..")
|
||||
continue
|
||||
}
|
||||
@@ -164,21 +163,6 @@ func decryptData(edata []byte, creds ...auth.Credentials) ([]byte, error) {
|
||||
return data, err
|
||||
}
|
||||
|
||||
func getOldCreds() (activeCredOld auth.Credentials, err error) {
|
||||
accessKeyOld := env.Get(config.EnvAccessKeyOld, "")
|
||||
secretKeyOld := env.Get(config.EnvSecretKeyOld, "")
|
||||
if accessKeyOld != "" && secretKeyOld != "" {
|
||||
activeCredOld, err = auth.CreateCredentials(accessKeyOld, secretKeyOld)
|
||||
if err != nil {
|
||||
return activeCredOld, err
|
||||
}
|
||||
// Once we have obtained the rotating creds
|
||||
os.Unsetenv(config.EnvAccessKeyOld)
|
||||
os.Unsetenv(config.EnvSecretKeyOld)
|
||||
}
|
||||
return activeCredOld, nil
|
||||
}
|
||||
|
||||
func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout)
|
||||
defer cancel()
|
||||
@@ -206,20 +190,15 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
||||
}
|
||||
}
|
||||
|
||||
activeCredOld, err := getOldCreds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if encrypted {
|
||||
// No key rotation requested, and backend is
|
||||
// already encrypted. We proceed without migration.
|
||||
if !activeCredOld.IsValid() {
|
||||
if !globalOldCred.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// No real reason to rotate if old and new creds are same.
|
||||
if activeCredOld.Equal(globalActiveCred) {
|
||||
if globalOldCred.Equal(globalActiveCred) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -254,8 +233,8 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
||||
|
||||
var data []byte
|
||||
// Is rotating of creds requested?
|
||||
if activeCredOld.IsValid() {
|
||||
data, err = decryptData(cdata, activeCredOld, globalActiveCred)
|
||||
if globalOldCred.IsValid() {
|
||||
data, err = decryptData(cdata, globalOldCred, globalActiveCred)
|
||||
if err != nil {
|
||||
if err == madmin.ErrMaliciousData {
|
||||
return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil)
|
||||
@@ -285,7 +264,7 @@ func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error {
|
||||
}
|
||||
}
|
||||
|
||||
if encrypted && globalActiveCred.IsValid() && activeCredOld.IsValid() {
|
||||
if encrypted && globalActiveCred.IsValid() && globalOldCred.IsValid() {
|
||||
logger.Info("Rotation complete, please make sure to unset MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs")
|
||||
}
|
||||
|
||||
|
||||
+1
-29
@@ -20,7 +20,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
@@ -212,33 +210,7 @@ func (sys *ConfigSys) Init(objAPI ObjectLayer) error {
|
||||
return errInvalidArgument
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
// Initializing configuration needs a retry mechanism for
|
||||
// the following reasons:
|
||||
// - Read quorum is lost just after the initialization
|
||||
// of the object layer.
|
||||
// - Write quorum not met when upgrading configuration
|
||||
// version is needed.
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
if err := initConfig(objAPI); err != nil {
|
||||
if err == errDiskNotFound ||
|
||||
strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) ||
|
||||
strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) {
|
||||
logger.Info("Waiting for configuration to be initialized..")
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Initializing config sub-system gracefully stopped")
|
||||
}
|
||||
}
|
||||
return initConfig(objAPI)
|
||||
}
|
||||
|
||||
// NewConfigSys - creates new config system object.
|
||||
|
||||
Vendored
+21
-7
@@ -28,12 +28,15 @@ import (
|
||||
|
||||
// Config represents cache config settings
|
||||
type Config struct {
|
||||
Enabled bool `json:"-"`
|
||||
Drives []string `json:"drives"`
|
||||
Expiry int `json:"expiry"`
|
||||
MaxUse int `json:"maxuse"`
|
||||
Quota int `json:"quota"`
|
||||
Exclude []string `json:"exclude"`
|
||||
Enabled bool `json:"-"`
|
||||
Drives []string `json:"drives"`
|
||||
Expiry int `json:"expiry"`
|
||||
MaxUse int `json:"maxuse"`
|
||||
Quota int `json:"quota"`
|
||||
Exclude []string `json:"exclude"`
|
||||
After int `json:"after"`
|
||||
WatermarkLow int `json:"watermark_low"`
|
||||
WatermarkHigh int `json:"watermark_high"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON - implements JSON unmarshal interface for unmarshalling
|
||||
@@ -60,7 +63,18 @@ func (cfg *Config) UnmarshalJSON(data []byte) (err error) {
|
||||
if _cfg.Quota < 0 {
|
||||
return errors.New("config quota value should not be null or negative")
|
||||
}
|
||||
|
||||
if _cfg.After < 0 {
|
||||
return errors.New("cache after value should not be less than 0")
|
||||
}
|
||||
if _cfg.WatermarkLow < 0 || _cfg.WatermarkLow > 100 {
|
||||
return errors.New("config low watermark value should be between 0 and 100")
|
||||
}
|
||||
if _cfg.WatermarkHigh < 0 || _cfg.WatermarkHigh > 100 {
|
||||
return errors.New("config high watermark value should be between 0 and 100")
|
||||
}
|
||||
if _cfg.WatermarkLow > 0 && (_cfg.WatermarkLow >= _cfg.WatermarkHigh) {
|
||||
return errors.New("config low watermark value should be less than high watermark")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Vendored
+18
@@ -50,5 +50,23 @@ var (
|
||||
Optional: true,
|
||||
Type: "sentence",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: After,
|
||||
Description: `minimum accesses before caching an object`,
|
||||
Optional: true,
|
||||
Type: "number",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: WatermarkLow,
|
||||
Description: `% of cache use at which to stop cache eviction`,
|
||||
Optional: true,
|
||||
Type: "number",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: WatermarkHigh,
|
||||
Description: `% of cache use at which to start cache eviction`,
|
||||
Optional: true,
|
||||
Type: "number",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
Vendored
+74
-13
@@ -26,21 +26,31 @@ import (
|
||||
|
||||
// Cache ENVs
|
||||
const (
|
||||
Drives = "drives"
|
||||
Exclude = "exclude"
|
||||
Expiry = "expiry"
|
||||
MaxUse = "maxuse"
|
||||
Quota = "quota"
|
||||
Drives = "drives"
|
||||
Exclude = "exclude"
|
||||
Expiry = "expiry"
|
||||
MaxUse = "maxuse"
|
||||
Quota = "quota"
|
||||
After = "after"
|
||||
WatermarkLow = "watermark_low"
|
||||
WatermarkHigh = "watermark_high"
|
||||
|
||||
EnvCacheDrives = "MINIO_CACHE_DRIVES"
|
||||
EnvCacheExclude = "MINIO_CACHE_EXCLUDE"
|
||||
EnvCacheExpiry = "MINIO_CACHE_EXPIRY"
|
||||
EnvCacheMaxUse = "MINIO_CACHE_MAXUSE"
|
||||
EnvCacheQuota = "MINIO_CACHE_QUOTA"
|
||||
EnvCacheAfter = "MINIO_CACHE_AFTER"
|
||||
EnvCacheWatermarkLow = "MINIO_CACHE_WATERMARK_LOW"
|
||||
EnvCacheWatermarkHigh = "MINIO_CACHE_WATERMARK_HIGH"
|
||||
|
||||
EnvCacheDrives = "MINIO_CACHE_DRIVES"
|
||||
EnvCacheExclude = "MINIO_CACHE_EXCLUDE"
|
||||
EnvCacheExpiry = "MINIO_CACHE_EXPIRY"
|
||||
EnvCacheMaxUse = "MINIO_CACHE_MAXUSE"
|
||||
EnvCacheQuota = "MINIO_CACHE_QUOTA"
|
||||
EnvCacheEncryptionMasterKey = "MINIO_CACHE_ENCRYPTION_MASTER_KEY"
|
||||
|
||||
DefaultExpiry = "90"
|
||||
DefaultQuota = "80"
|
||||
DefaultExpiry = "90"
|
||||
DefaultQuota = "80"
|
||||
DefaultAfter = "0"
|
||||
DefaultWaterMarkLow = "70"
|
||||
DefaultWaterMarkHigh = "80"
|
||||
)
|
||||
|
||||
// DefaultKVS - default KV settings for caching.
|
||||
@@ -62,6 +72,18 @@ var (
|
||||
Key: Quota,
|
||||
Value: DefaultQuota,
|
||||
},
|
||||
config.KV{
|
||||
Key: After,
|
||||
Value: DefaultAfter,
|
||||
},
|
||||
config.KV{
|
||||
Key: WatermarkLow,
|
||||
Value: DefaultWaterMarkLow,
|
||||
},
|
||||
config.KV{
|
||||
Key: WatermarkHigh,
|
||||
Value: DefaultWaterMarkHigh,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -79,7 +101,6 @@ func Enabled(kvs config.KVS) bool {
|
||||
// variables and merge them with provided CacheConfiguration.
|
||||
func LookupConfig(kvs config.KVS) (Config, error) {
|
||||
cfg := Config{}
|
||||
|
||||
if err := config.CheckValidKeys(config.CacheSubSys, kvs, DefaultKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
@@ -134,5 +155,45 @@ func LookupConfig(kvs config.KVS) (Config, error) {
|
||||
cfg.MaxUse = cfg.Quota
|
||||
}
|
||||
|
||||
if afterStr := env.Get(EnvCacheAfter, kvs.Get(After)); afterStr != "" {
|
||||
cfg.After, err = strconv.Atoi(afterStr)
|
||||
if err != nil {
|
||||
return cfg, config.ErrInvalidCacheAfter(err)
|
||||
}
|
||||
// after should be a valid value >= 0.
|
||||
if cfg.After < 0 {
|
||||
err := errors.New("cache after value cannot be less than 0")
|
||||
return cfg, config.ErrInvalidCacheAfter(err)
|
||||
}
|
||||
}
|
||||
|
||||
if lowWMStr := env.Get(EnvCacheWatermarkLow, kvs.Get(WatermarkLow)); lowWMStr != "" {
|
||||
cfg.WatermarkLow, err = strconv.Atoi(lowWMStr)
|
||||
if err != nil {
|
||||
return cfg, config.ErrInvalidCacheWatermarkLow(err)
|
||||
}
|
||||
// WatermarkLow should be a valid percentage.
|
||||
if cfg.WatermarkLow < 0 || cfg.WatermarkLow > 100 {
|
||||
err := errors.New("config min watermark value should be between 0 and 100")
|
||||
return cfg, config.ErrInvalidCacheWatermarkLow(err)
|
||||
}
|
||||
}
|
||||
|
||||
if highWMStr := env.Get(EnvCacheWatermarkHigh, kvs.Get(WatermarkHigh)); highWMStr != "" {
|
||||
cfg.WatermarkHigh, err = strconv.Atoi(highWMStr)
|
||||
if err != nil {
|
||||
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||
}
|
||||
|
||||
// MaxWatermark should be a valid percentage.
|
||||
if cfg.WatermarkHigh < 0 || cfg.WatermarkHigh > 100 {
|
||||
err := errors.New("config high watermark value should be between 0 and 100")
|
||||
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||
}
|
||||
}
|
||||
if cfg.WatermarkLow > cfg.WatermarkHigh {
|
||||
err := errors.New("config high watermark value should be greater than low watermark value")
|
||||
return cfg, config.ErrInvalidCacheWatermarkHigh(err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@ const (
|
||||
EnvPublicIPs = "MINIO_PUBLIC_IPS"
|
||||
EnvEndpoints = "MINIO_ENDPOINTS"
|
||||
|
||||
// API sub-system
|
||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||
|
||||
EnvUpdate = "MINIO_UPDATE"
|
||||
|
||||
EnvWorm = "MINIO_WORM" // legacy
|
||||
|
||||
@@ -46,7 +46,7 @@ func (u Err) Clone() Err {
|
||||
}
|
||||
}
|
||||
|
||||
// Return the error message
|
||||
// Error returns the error message
|
||||
func (u Err) Error() string {
|
||||
if u.detail == "" {
|
||||
if u.msg != "" {
|
||||
|
||||
@@ -66,6 +66,24 @@ var (
|
||||
"MINIO_CACHE_QUOTA: Valid cache quota value must be between 0-100",
|
||||
)
|
||||
|
||||
ErrInvalidCacheAfter = newErrFn(
|
||||
"Invalid cache after value",
|
||||
"Please check the passed value",
|
||||
"MINIO_CACHE_AFTER: Valid cache after value must be 0 or greater",
|
||||
)
|
||||
|
||||
ErrInvalidCacheWatermarkLow = newErrFn(
|
||||
"Invalid cache low watermark value",
|
||||
"Please check the passed value",
|
||||
"MINIO_CACHE_WATERMARK_LOW: Valid cache low watermark value must be between 0-100",
|
||||
)
|
||||
|
||||
ErrInvalidCacheWatermarkHigh = newErrFn(
|
||||
"Invalid cache high watermark value",
|
||||
"Please check the passed value",
|
||||
"MINIO_CACHE_WATERMARK_HIGH: Valid cache high watermark value must be between 0-100",
|
||||
)
|
||||
|
||||
ErrInvalidCacheEncryptionKey = newErrFn(
|
||||
"Invalid cache encryption master key value",
|
||||
"Please check the passed value",
|
||||
|
||||
@@ -38,18 +38,18 @@ var ErrNoEntriesFound = errors.New("No entries found for this key")
|
||||
const etcdPathSeparator = "/"
|
||||
|
||||
// create a new coredns service record for the bucket.
|
||||
func newCoreDNSMsg(ip string, port string, ttl uint32) ([]byte, error) {
|
||||
func newCoreDNSMsg(ip string, port string, ttl uint32, t time.Time) ([]byte, error) {
|
||||
return json.Marshal(&SrvRecord{
|
||||
Host: ip,
|
||||
Port: json.Number(port),
|
||||
TTL: ttl,
|
||||
CreationDate: time.Now().UTC(),
|
||||
CreationDate: t,
|
||||
})
|
||||
}
|
||||
|
||||
// List - Retrieves list of DNS entries for the domain.
|
||||
func (c *CoreDNS) List() ([]SrvRecord, error) {
|
||||
var srvRecords []SrvRecord
|
||||
func (c *CoreDNS) List() (map[string][]SrvRecord, error) {
|
||||
var srvRecords = map[string][]SrvRecord{}
|
||||
for _, domainName := range c.domainNames {
|
||||
key := msg.Path(fmt.Sprintf("%s.", domainName), c.prefixPath)
|
||||
records, err := c.list(key)
|
||||
@@ -60,7 +60,7 @@ func (c *CoreDNS) List() ([]SrvRecord, error) {
|
||||
if record.Key == "" {
|
||||
continue
|
||||
}
|
||||
srvRecords = append(srvRecords, record)
|
||||
srvRecords[record.Key] = append(srvRecords[record.Key], record)
|
||||
}
|
||||
}
|
||||
return srvRecords, nil
|
||||
@@ -150,8 +150,9 @@ func (c *CoreDNS) list(key string) ([]SrvRecord, error) {
|
||||
func (c *CoreDNS) Put(bucket string) error {
|
||||
c.Delete(bucket) // delete any existing entries.
|
||||
|
||||
t := time.Now().UTC()
|
||||
for ip := range c.domainIPs {
|
||||
bucketMsg, err := newCoreDNSMsg(ip, c.domainPort, defaultTTL)
|
||||
bucketMsg, err := newCoreDNSMsg(ip, c.domainPort, defaultTTL, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+194
-103
@@ -21,7 +21,8 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/config"
|
||||
@@ -44,34 +45,45 @@ type Config struct {
|
||||
STSExpiryDuration string `json:"stsExpiryDuration"`
|
||||
|
||||
// Format string for usernames
|
||||
UsernameFormat string `json:"usernameFormat"`
|
||||
UsernameFormat string `json:"usernameFormat"`
|
||||
UsernameFormats []string `json:"-"`
|
||||
UsernameSearchFilter string `json:"-"`
|
||||
UsernameSearchBaseDNS []string `json:"-"`
|
||||
|
||||
GroupSearchBaseDN string `json:"groupSearchBaseDN"`
|
||||
GroupSearchFilter string `json:"groupSearchFilter"`
|
||||
GroupNameAttribute string `json:"groupNameAttribute"`
|
||||
GroupSearchBaseDN string `json:"groupSearchBaseDN"`
|
||||
GroupSearchBaseDNS []string `json:"-"`
|
||||
GroupSearchFilter string `json:"groupSearchFilter"`
|
||||
GroupNameAttribute string `json:"groupNameAttribute"`
|
||||
|
||||
stsExpiryDuration time.Duration // contains converted value
|
||||
tlsSkipVerify bool // allows skipping TLS verification
|
||||
serverInsecure bool // allows plain text connection to LDAP Server
|
||||
rootCAs *x509.CertPool
|
||||
}
|
||||
|
||||
// LDAP keys and envs.
|
||||
const (
|
||||
ServerAddr = "server_addr"
|
||||
STSExpiry = "sts_expiry"
|
||||
UsernameFormat = "username_format"
|
||||
GroupSearchFilter = "group_search_filter"
|
||||
GroupNameAttribute = "group_name_attribute"
|
||||
GroupSearchBaseDN = "group_search_base_dn"
|
||||
TLSSkipVerify = "tls_skip_verify"
|
||||
ServerAddr = "server_addr"
|
||||
STSExpiry = "sts_expiry"
|
||||
UsernameFormat = "username_format"
|
||||
UsernameSearchFilter = "username_search_filter"
|
||||
UsernameSearchBaseDN = "username_search_base_dn"
|
||||
GroupSearchFilter = "group_search_filter"
|
||||
GroupNameAttribute = "group_name_attribute"
|
||||
GroupSearchBaseDN = "group_search_base_dn"
|
||||
TLSSkipVerify = "tls_skip_verify"
|
||||
ServerInsecure = "server_insecure"
|
||||
|
||||
EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR"
|
||||
EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY"
|
||||
EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"
|
||||
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
|
||||
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
|
||||
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
|
||||
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
|
||||
EnvServerAddr = "MINIO_IDENTITY_LDAP_SERVER_ADDR"
|
||||
EnvSTSExpiry = "MINIO_IDENTITY_LDAP_STS_EXPIRY"
|
||||
EnvTLSSkipVerify = "MINIO_IDENTITY_LDAP_TLS_SKIP_VERIFY"
|
||||
EnvServerInsecure = "MINIO_IDENTITY_LDAP_SERVER_INSECURE"
|
||||
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
|
||||
EnvUsernameSearchFilter = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"
|
||||
EnvUsernameSearchBaseDN = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN"
|
||||
EnvGroupSearchFilter = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_FILTER"
|
||||
EnvGroupNameAttribute = "MINIO_IDENTITY_LDAP_GROUP_NAME_ATTRIBUTE"
|
||||
EnvGroupSearchBaseDN = "MINIO_IDENTITY_LDAP_GROUP_SEARCH_BASE_DN"
|
||||
)
|
||||
|
||||
// DefaultKVS - default config for LDAP config
|
||||
@@ -82,11 +94,15 @@ var (
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: STSExpiry,
|
||||
Value: "1h",
|
||||
Key: UsernameFormat,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: UsernameFormat,
|
||||
Key: UsernameSearchFilter,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: UsernameSearchBaseDN,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
@@ -101,19 +117,146 @@ var (
|
||||
Key: GroupSearchBaseDN,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: STSExpiry,
|
||||
Value: "1h",
|
||||
},
|
||||
config.KV{
|
||||
Key: TLSSkipVerify,
|
||||
Value: config.EnableOff,
|
||||
},
|
||||
config.KV{
|
||||
Key: ServerInsecure,
|
||||
Value: config.EnableOff,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
dnDelimiter = ";"
|
||||
)
|
||||
|
||||
func getGroups(conn *ldap.Conn, sreq *ldap.SearchRequest) ([]string, error) {
|
||||
var groups []string
|
||||
sres, err := conn.Search(sreq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range sres.Entries {
|
||||
// We only queried one attribute,
|
||||
// so we only look up the first one.
|
||||
groups = append(groups, entry.Attributes[0].Values...)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (l *Config) bind(conn *ldap.Conn, username, password string) ([]string, error) {
|
||||
var bindDNS = make([]string, len(l.UsernameFormats))
|
||||
for i, usernameFormat := range l.UsernameFormats {
|
||||
bindDN := fmt.Sprintf(usernameFormat, username)
|
||||
// Bind with user credentials to validate the password
|
||||
if err := conn.Bind(bindDN, password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindDNS[i] = bindDN
|
||||
}
|
||||
return bindDNS, nil
|
||||
}
|
||||
|
||||
var standardAttributes = []string{
|
||||
"givenName",
|
||||
"sn",
|
||||
"cn",
|
||||
"memberOf",
|
||||
"email",
|
||||
}
|
||||
|
||||
// Bind - binds to ldap, searches LDAP and returns list of groups.
|
||||
func (l *Config) Bind(username, password string) ([]string, error) {
|
||||
conn, err := l.Connect()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
bindDNS, err := l.bind(conn, username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groups []string
|
||||
if l.UsernameSearchFilter != "" {
|
||||
for _, userSearchBase := range l.UsernameSearchBaseDNS {
|
||||
filter := strings.Replace(l.UsernameSearchFilter, "%s",
|
||||
ldap.EscapeFilter(username), -1)
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
userSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
standardAttributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
groups, err = getGroups(conn, searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if l.GroupSearchFilter != "" {
|
||||
for _, groupSearchBase := range l.GroupSearchBaseDNS {
|
||||
var filters []string
|
||||
if l.GroupNameAttribute == "" {
|
||||
filters = []string{strings.Replace(l.GroupSearchFilter, "%s",
|
||||
ldap.EscapeFilter(username), -1)}
|
||||
} else {
|
||||
// With group name attribute specified, make sure to
|
||||
// include search queries for CN distinguished name
|
||||
for _, bindDN := range bindDNS {
|
||||
filters = append(filters, strings.Replace(l.GroupSearchFilter, "%s",
|
||||
ldap.EscapeFilter(bindDN), -1))
|
||||
}
|
||||
}
|
||||
for _, filter := range filters {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
groupSearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
standardAttributes,
|
||||
nil,
|
||||
)
|
||||
|
||||
var newGroups []string
|
||||
newGroups, err = getGroups(conn, searchRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups = append(groups, newGroups...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
// Connect connect to ldap server.
|
||||
func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
if l == nil {
|
||||
// Happens when LDAP is not configured.
|
||||
return
|
||||
return nil, errors.New("LDAP is not configured")
|
||||
}
|
||||
|
||||
if _, _, err = net.SplitHostPort(l.ServerAddr); err != nil {
|
||||
// User default LDAP port if none specified "636"
|
||||
l.ServerAddr = net.JoinHostPort(l.ServerAddr, "636")
|
||||
}
|
||||
|
||||
if l.serverInsecure {
|
||||
return ldap.Dial("tcp", l.ServerAddr)
|
||||
}
|
||||
|
||||
return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{
|
||||
InsecureSkipVerify: l.tlsSkipVerify,
|
||||
RootCAs: l.rootCAs,
|
||||
@@ -154,6 +297,12 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
l.STSExpiryDuration = v
|
||||
l.stsExpiryDuration = expDur
|
||||
}
|
||||
if v := env.Get(EnvServerInsecure, kvs.Get(ServerInsecure)); v != "" {
|
||||
l.serverInsecure, err = config.ParseBool(v)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
}
|
||||
if v := env.Get(EnvTLSSkipVerify, kvs.Get(TLSSkipVerify)); v != "" {
|
||||
l.tlsSkipVerify, err = config.ParseBool(v)
|
||||
if err != nil {
|
||||
@@ -161,102 +310,44 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
}
|
||||
}
|
||||
if v := env.Get(EnvUsernameFormat, kvs.Get(UsernameFormat)); v != "" {
|
||||
subs, err := NewSubstituter("username", "test")
|
||||
if err != nil {
|
||||
return l, err
|
||||
if !strings.Contains(v, "%s") {
|
||||
return l, errors.New("LDAP username format doesn't have '%s' substitution")
|
||||
}
|
||||
if _, err := subs.Substitute(v); err != nil {
|
||||
return l, err
|
||||
}
|
||||
l.UsernameFormat = v
|
||||
l.UsernameFormats = strings.Split(v, dnDelimiter)
|
||||
} else {
|
||||
return l, fmt.Errorf("'%s' cannot be empty and must have a value", UsernameFormat)
|
||||
}
|
||||
|
||||
if v := env.Get(EnvUsernameSearchFilter, kvs.Get(UsernameSearchFilter)); v != "" {
|
||||
if !strings.Contains(v, "%s") {
|
||||
return l, errors.New("LDAP username search filter doesn't have '%s' substitution")
|
||||
}
|
||||
l.UsernameSearchFilter = v
|
||||
}
|
||||
|
||||
if v := env.Get(EnvUsernameSearchBaseDN, kvs.Get(UsernameSearchBaseDN)); v != "" {
|
||||
l.UsernameSearchBaseDNS = strings.Split(v, dnDelimiter)
|
||||
}
|
||||
|
||||
grpSearchFilter := env.Get(EnvGroupSearchFilter, kvs.Get(GroupSearchFilter))
|
||||
grpSearchNameAttr := env.Get(EnvGroupNameAttribute, kvs.Get(GroupNameAttribute))
|
||||
grpSearchBaseDN := env.Get(EnvGroupSearchBaseDN, kvs.Get(GroupSearchBaseDN))
|
||||
|
||||
// Either all group params must be set or none must be set.
|
||||
allNotSet := grpSearchFilter == "" && grpSearchNameAttr == "" && grpSearchBaseDN == ""
|
||||
allSet := grpSearchFilter != "" && grpSearchNameAttr != "" && grpSearchBaseDN != ""
|
||||
if !allNotSet && !allSet {
|
||||
return l, errors.New("All group related parameters must be set")
|
||||
var allSet bool
|
||||
if grpSearchFilter != "" {
|
||||
if grpSearchNameAttr == "" || grpSearchBaseDN == "" {
|
||||
return l, errors.New("All group related parameters must be set")
|
||||
}
|
||||
allSet = true
|
||||
}
|
||||
|
||||
if allSet {
|
||||
subs, err := NewSubstituter("username", "test", "usernamedn", "test2")
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
if _, err := subs.Substitute(grpSearchFilter); err != nil {
|
||||
return l, fmt.Errorf("Only username and usernamedn may be substituted in the group search filter string: %s", err)
|
||||
}
|
||||
l.GroupSearchFilter = grpSearchFilter
|
||||
l.GroupNameAttribute = grpSearchNameAttr
|
||||
subs, err = NewSubstituter("username", "test", "usernamedn", "test2")
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
if _, err := subs.Substitute(grpSearchBaseDN); err != nil {
|
||||
return l, fmt.Errorf("Only username and usernamedn may be substituted in the base DN string: %s", err)
|
||||
}
|
||||
l.GroupSearchBaseDN = grpSearchBaseDN
|
||||
l.GroupSearchBaseDNS = strings.Split(grpSearchBaseDN, dnDelimiter)
|
||||
}
|
||||
|
||||
l.rootCAs = rootCAs
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Substituter - This type is to allow restricted runtime
|
||||
// substitutions of variables in LDAP configuration items during
|
||||
// runtime.
|
||||
type Substituter struct {
|
||||
vals map[string]string
|
||||
}
|
||||
|
||||
// NewSubstituter - sets up the substituter for usage, for e.g.:
|
||||
//
|
||||
// subber := NewSubstituter("username", "john")
|
||||
func NewSubstituter(v ...string) (Substituter, error) {
|
||||
if len(v)%2 != 0 {
|
||||
return Substituter{}, errors.New("Need an even number of arguments")
|
||||
}
|
||||
vals := make(map[string]string)
|
||||
for i := 0; i < len(v); i += 2 {
|
||||
vals[v[i]] = v[i+1]
|
||||
}
|
||||
return Substituter{vals: vals}, nil
|
||||
}
|
||||
|
||||
// Substitute - performs substitution on the given string `t`. Returns
|
||||
// an error if there are any variables in the input that do not have
|
||||
// values in the substituter. E.g.:
|
||||
//
|
||||
// subber.Substitute("uid=${username},cn=users,dc=example,dc=com")
|
||||
//
|
||||
// or
|
||||
//
|
||||
// subber.Substitute("uid={username},cn=users,dc=example,dc=com")
|
||||
//
|
||||
// returns "uid=john,cn=users,dc=example,dc=com"
|
||||
//
|
||||
// whereas:
|
||||
//
|
||||
// subber.Substitute("uid=${usernamedn}")
|
||||
//
|
||||
// returns an error.
|
||||
func (s *Substituter) Substitute(t string) (string, error) {
|
||||
for k, v := range s.vals {
|
||||
reDollar := regexp.MustCompile(fmt.Sprintf(`\$\{%s\}`, k))
|
||||
t = reDollar.ReplaceAllLiteralString(t, v)
|
||||
reFlower := regexp.MustCompile(fmt.Sprintf(`\{%s\}`, k))
|
||||
t = reFlower.ReplaceAllLiteralString(t, v)
|
||||
}
|
||||
// Check if all requested substitutions have been made.
|
||||
re := regexp.MustCompile(`\{.*\}`)
|
||||
if re.MatchString(t) {
|
||||
return "", errors.New("unsupported substitution requested")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSubstituter(t *testing.T) {
|
||||
tests := []struct {
|
||||
KV []string
|
||||
SubstitutableStr string
|
||||
SubstitutedStr string
|
||||
ErrExpected bool
|
||||
}{
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "uid=${username},cn=users,dc=example,dc=com",
|
||||
SubstitutedStr: "uid=john,cn=users,dc=example,dc=com",
|
||||
ErrExpected: false,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "uid={username},cn=users,dc=example,dc=com",
|
||||
SubstitutedStr: "uid=john,cn=users,dc=example,dc=com",
|
||||
ErrExpected: false,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "(&(objectclass=group)(member=${username}))",
|
||||
SubstitutedStr: "(&(objectclass=group)(member=john))",
|
||||
ErrExpected: false,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "(&(objectclass=group)(member={username}))",
|
||||
SubstitutedStr: "(&(objectclass=group)(member=john))",
|
||||
ErrExpected: false,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "uid=${{username}},cn=users,dc=example,dc=com",
|
||||
ErrExpected: true,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com",
|
||||
ErrExpected: true,
|
||||
},
|
||||
{
|
||||
KV: []string{"username"},
|
||||
SubstitutableStr: "uid=${usernamedn},cn=users,dc=example,dc=com",
|
||||
ErrExpected: true,
|
||||
},
|
||||
{
|
||||
KV: []string{"username", "john"},
|
||||
SubstitutableStr: "(&(objectclass=user)(sAMAccountName={username})(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))",
|
||||
SubstitutedStr: "(&(objectclass=user)(sAMAccountName=john)(memberOf=CN=myorg,OU=Rialto,OU=Application Managed,OU=Groups,DC=amr,DC=corp,DC=myorg,DC=com))",
|
||||
ErrExpected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.SubstitutableStr, func(t *testing.T) {
|
||||
subber, err := NewSubstituter(test.KV...)
|
||||
if err != nil && !test.ErrExpected {
|
||||
t.Errorf("Unexpected failure %s", err)
|
||||
}
|
||||
gotStr, err := subber.Substitute(test.SubstitutableStr)
|
||||
if err != nil && !test.ErrExpected {
|
||||
t.Errorf("Unexpected failure %s", err)
|
||||
}
|
||||
if gotStr != test.SubstitutedStr {
|
||||
t.Errorf("Expected %s, got %s", test.SubstitutedStr, gotStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,24 +28,33 @@ var (
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: UsernameFormat,
|
||||
Description: `username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`,
|
||||
Description: `";" separated list of username bind DNs e.g. "uid=%s,cn=accounts,dc=myldapserver,dc=com"`,
|
||||
Type: "list",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: UsernameSearchFilter,
|
||||
Description: `user search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)"`,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: GroupSearchFilter,
|
||||
Description: `search filter for groups e.g. "(&(objectclass=groupOfNames)(memberUid=%s))"`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: GroupNameAttribute,
|
||||
Description: `search attribute for group name e.g. "cn"`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: GroupSearchBaseDN,
|
||||
Description: `group search base DNs e.g. "dc=myldapserver,dc=com"`,
|
||||
Description: `";" separated list of group search base DNs e.g. "dc=myldapserver,dc=com"`,
|
||||
Type: "list",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: UsernameSearchBaseDN,
|
||||
Description: `";" separated list of username search DNs`,
|
||||
Type: "list",
|
||||
Optional: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: GroupNameAttribute,
|
||||
Description: `search attribute for group name e.g. "cn"`,
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
@@ -57,7 +66,13 @@ var (
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: TLSSkipVerify,
|
||||
Description: `trust server TLS without verification, defaults to "on" (verify)`,
|
||||
Description: `trust server TLS without verification, defaults to "off" (verify)`,
|
||||
Optional: true,
|
||||
Type: "on|off",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: ServerInsecure,
|
||||
Description: `allow plain text connection to AD/LDAP server, defaults to "off"`,
|
||||
Optional: true,
|
||||
Type: "on|off",
|
||||
},
|
||||
|
||||
@@ -251,6 +251,7 @@ func parseDiscoveryDoc(u *xnet.URL, transport *http.Transport, closeRespFn func(
|
||||
}
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
clnt.CloseIdleConnections()
|
||||
return d, err
|
||||
}
|
||||
defer closeRespFn(resp.Body)
|
||||
|
||||
@@ -165,6 +165,12 @@ var (
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: target.KafkaSASLMechanism,
|
||||
Description: "sasl authentication mechanism, default 'plain'",
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: target.KafkaTLSClientAuth,
|
||||
Description: "clientAuth determines the Kafka server's policy for TLS client auth",
|
||||
@@ -213,6 +219,12 @@ var (
|
||||
Optional: true,
|
||||
Type: "number",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: target.KafkaVersion,
|
||||
Description: "specify the version of the Kafka cluster",
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.Comment,
|
||||
Description: config.DefaultComment,
|
||||
@@ -352,6 +364,7 @@ var (
|
||||
config.HelpKV{
|
||||
Key: target.MySQLDSNString,
|
||||
Description: "MySQL data-source-name connection string",
|
||||
Optional: true,
|
||||
Type: "string",
|
||||
},
|
||||
config.HelpKV{
|
||||
|
||||
+39
-34
@@ -42,7 +42,14 @@ const (
|
||||
func TestNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport,
|
||||
targetIDs []event.TargetID) error {
|
||||
test := true
|
||||
_, err := RegisterNotificationTargets(cfg, doneCh, transport, targetIDs, test)
|
||||
targets, err := RegisterNotificationTargets(cfg, doneCh, transport, targetIDs, test)
|
||||
if err == nil {
|
||||
// Close all targets since we are only testing connections.
|
||||
for _, t := range targets.TargetMap() {
|
||||
_ = t.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -58,9 +65,19 @@ func GetNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport
|
||||
// * Add a new target in pkg/event/target package.
|
||||
// * Add newly added target configuration to serverConfig.Notify.<TARGET_NAME>.
|
||||
// * Handle the configuration in this function to create/add into TargetList.
|
||||
func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, targetIDs []event.TargetID, test bool) (*event.TargetList, error) {
|
||||
func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, transport *http.Transport, targetIDs []event.TargetID, test bool) (_ *event.TargetList, err error) {
|
||||
targetList := event.NewTargetList()
|
||||
if err := checkValidNotificationKeys(cfg); err != nil {
|
||||
|
||||
defer func() {
|
||||
// Automatically close all connections to targets when an error occur
|
||||
if err != nil {
|
||||
for _, t := range targetList.TargetMap() {
|
||||
_ = t.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = checkValidNotificationKeys(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -125,9 +142,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range esTargets {
|
||||
@@ -142,9 +156,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range kafkaTargets {
|
||||
@@ -159,9 +170,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range mqttTargets {
|
||||
@@ -176,9 +184,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range mysqlTargets {
|
||||
@@ -192,9 +197,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range natsTargets {
|
||||
@@ -208,9 +210,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range nsqTargets {
|
||||
@@ -224,9 +223,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range postgresTargets {
|
||||
@@ -240,9 +236,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range redisTargets {
|
||||
@@ -256,9 +249,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err = targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
}
|
||||
}
|
||||
|
||||
for id, args := range webhookTargets {
|
||||
@@ -272,10 +262,6 @@ func RegisterNotificationTargets(cfg config.Config, doneCh <-chan struct{}, tran
|
||||
if err := targetList.Add(newTarget); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if test {
|
||||
newTarget.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if test {
|
||||
@@ -366,6 +352,10 @@ var (
|
||||
Key: target.KafkaSASLPassword,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: target.KafkaSASLMechanism,
|
||||
Value: "plain",
|
||||
},
|
||||
config.KV{
|
||||
Key: target.KafkaClientTLSCert,
|
||||
Value: "",
|
||||
@@ -398,6 +388,10 @@ var (
|
||||
Key: target.KafkaQueueDir,
|
||||
Value: "",
|
||||
},
|
||||
config.KV{
|
||||
Key: target.KafkaVersion,
|
||||
Value: "",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -465,12 +459,18 @@ func GetNotifyKafka(kafkaKVS map[string]config.KVS) (map[string]target.KafkaArgs
|
||||
queueDirEnv = queueDirEnv + config.Default + k
|
||||
}
|
||||
|
||||
versionEnv := target.EnvKafkaVersion
|
||||
if k != config.Default {
|
||||
versionEnv = versionEnv + config.Default + k
|
||||
}
|
||||
|
||||
kafkaArgs := target.KafkaArgs{
|
||||
Enable: enabled,
|
||||
Brokers: brokers,
|
||||
Topic: env.Get(topicEnv, kv.Get(target.KafkaTopic)),
|
||||
QueueDir: env.Get(queueDirEnv, kv.Get(target.KafkaQueueDir)),
|
||||
QueueLimit: queueLimit,
|
||||
Version: env.Get(versionEnv, kv.Get(target.KafkaVersion)),
|
||||
}
|
||||
|
||||
tlsEnableEnv := target.EnvKafkaTLS
|
||||
@@ -511,9 +511,14 @@ func GetNotifyKafka(kafkaKVS map[string]config.KVS) (map[string]target.KafkaArgs
|
||||
if k != config.Default {
|
||||
saslPasswordEnv = saslPasswordEnv + config.Default + k
|
||||
}
|
||||
saslMechanismEnv := target.EnvKafkaSASLMechanism
|
||||
if k != config.Default {
|
||||
saslMechanismEnv = saslMechanismEnv + config.Default + k
|
||||
}
|
||||
kafkaArgs.SASL.Enable = env.Get(saslEnableEnv, kv.Get(target.KafkaSASL)) == config.EnableOn
|
||||
kafkaArgs.SASL.User = env.Get(saslUsernameEnv, kv.Get(target.KafkaSASLUsername))
|
||||
kafkaArgs.SASL.Password = env.Get(saslPasswordEnv, kv.Get(target.KafkaSASLPassword))
|
||||
kafkaArgs.SASL.Mechanism = env.Get(saslMechanismEnv, kv.Get(target.KafkaSASLMechanism))
|
||||
|
||||
if err = kafkaArgs.Validate(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -223,5 +223,6 @@ func (o *Opa) IsAllowed(args iampolicy.Args) (bool, error) {
|
||||
}
|
||||
return resultAllow.Result.Allow, nil
|
||||
}
|
||||
|
||||
return result.Result, nil
|
||||
}
|
||||
|
||||
+12
-10
@@ -24,7 +24,6 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
"github.com/minio/minio/cmd/logger/target/console"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/pubsub"
|
||||
)
|
||||
@@ -84,17 +83,20 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
||||
cnt := 0
|
||||
// by default send all console logs in the ring buffer unless node or limit query parameters
|
||||
// are set.
|
||||
var lastN []madmin.LogInfo
|
||||
var lastN []log.Info
|
||||
if last > defaultLogBufferCount || last <= 0 {
|
||||
last = defaultLogBufferCount
|
||||
}
|
||||
|
||||
lastN = make([]madmin.LogInfo, last)
|
||||
lastN = make([]log.Info, last)
|
||||
sys.RLock()
|
||||
sys.logBuf.Do(func(p interface{}) {
|
||||
if p != nil && (p.(madmin.LogInfo)).SendLog(node, logKind) {
|
||||
lastN[cnt%last] = p.(madmin.LogInfo)
|
||||
cnt++
|
||||
if p != nil {
|
||||
lg, ok := p.(log.Info)
|
||||
if ok && lg.SendLog(node, logKind) {
|
||||
lastN[cnt%last] = lg
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
})
|
||||
sys.RUnlock()
|
||||
@@ -102,7 +104,7 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
||||
if cnt > 0 {
|
||||
for i := 0; i < last; i++ {
|
||||
entry := lastN[(cnt+i)%last]
|
||||
if (entry == madmin.LogInfo{}) {
|
||||
if (entry == log.Info{}) {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
@@ -118,12 +120,12 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh chan s
|
||||
// Send log message 'e' to console and publish to console
|
||||
// log pubsub system
|
||||
func (sys *HTTPConsoleLoggerSys) Send(e interface{}, logKind string) error {
|
||||
var lg madmin.LogInfo
|
||||
var lg log.Info
|
||||
switch e := e.(type) {
|
||||
case log.Entry:
|
||||
lg = madmin.LogInfo{Entry: e, NodeName: sys.nodeName}
|
||||
lg = log.Info{Entry: e, NodeName: sys.nodeName}
|
||||
case string:
|
||||
lg = madmin.LogInfo{ConsoleMsg: e, NodeName: sys.nodeName}
|
||||
lg = log.Info{ConsoleMsg: e, NodeName: sys.nodeName}
|
||||
}
|
||||
|
||||
sys.pubsub.Publish(lg)
|
||||
|
||||
+33
-92
@@ -21,8 +21,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,90 +30,27 @@ const (
|
||||
bgLifecycleTick = time.Hour
|
||||
)
|
||||
|
||||
type lifecycleOps struct {
|
||||
LastActivity time.Time
|
||||
}
|
||||
|
||||
// Register to the daily objects listing
|
||||
var globalLifecycleOps = &lifecycleOps{}
|
||||
|
||||
func getLocalBgLifecycleOpsStatus() BgLifecycleOpsStatus {
|
||||
return BgLifecycleOpsStatus{
|
||||
LastActivity: globalLifecycleOps.LastActivity,
|
||||
}
|
||||
}
|
||||
|
||||
// initDailyLifecycle starts the routine that receives the daily
|
||||
// listing of all objects and applies any matching bucket lifecycle
|
||||
// rules.
|
||||
func initDailyLifecycle() {
|
||||
go startDailyLifecycle()
|
||||
func initDailyLifecycle(ctx context.Context, objAPI ObjectLayer) {
|
||||
go startDailyLifecycle(ctx, objAPI)
|
||||
}
|
||||
|
||||
func startDailyLifecycle() {
|
||||
var objAPI ObjectLayer
|
||||
var ctx = context.Background()
|
||||
|
||||
// Wait until the object API is ready
|
||||
func startDailyLifecycle(ctx context.Context, objAPI ObjectLayer) {
|
||||
for {
|
||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Calculate the time of the last lifecycle operation in all peers node of the cluster
|
||||
computeLastLifecycleActivity := func(status []BgOpsStatus) time.Time {
|
||||
var lastAct time.Time
|
||||
for _, st := range status {
|
||||
if st.LifecycleOps.LastActivity.After(lastAct) {
|
||||
lastAct = st.LifecycleOps.LastActivity
|
||||
}
|
||||
}
|
||||
return lastAct
|
||||
}
|
||||
|
||||
for {
|
||||
// Check if we should perform lifecycle ops based on the last lifecycle activity, sleep one hour otherwise
|
||||
allLifecycleStatus := []BgOpsStatus{
|
||||
{LifecycleOps: getLocalBgLifecycleOpsStatus()},
|
||||
}
|
||||
if globalIsDistXL {
|
||||
allLifecycleStatus = append(allLifecycleStatus, globalNotificationSys.BackgroundOpsStatus()...)
|
||||
}
|
||||
lastAct := computeLastLifecycleActivity(allLifecycleStatus)
|
||||
if !lastAct.IsZero() && time.Since(lastAct) < bgLifecycleInterval {
|
||||
time.Sleep(bgLifecycleTick)
|
||||
}
|
||||
|
||||
// Perform one lifecycle operation
|
||||
err := lifecycleRound(ctx, objAPI)
|
||||
switch err.(type) {
|
||||
// Unable to hold a lock means there is another
|
||||
// instance doing the lifecycle round round
|
||||
case OperationTimedOut:
|
||||
time.Sleep(bgLifecycleTick)
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
time.Sleep(time.Minute)
|
||||
continue
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.NewTimer(bgLifecycleInterval).C:
|
||||
// Perform one lifecycle operation
|
||||
logger.LogIf(ctx, lifecycleRound(ctx, objAPI))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var lifecycleTimeout = newDynamicTimeout(60*time.Second, time.Second)
|
||||
|
||||
func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
// Lock to avoid concurrent lifecycle ops from other nodes
|
||||
sweepLock := objAPI.NewNSLock(ctx, "system", "daily-lifecycle-ops")
|
||||
if err := sweepLock.GetLock(lifecycleTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer sweepLock.Unlock()
|
||||
|
||||
buckets, err := objAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -129,29 +66,39 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
// Calculate the common prefix of all lifecycle rules
|
||||
var prefixes []string
|
||||
for _, rule := range l.Rules {
|
||||
prefixes = append(prefixes, rule.Filter.Prefix)
|
||||
prefixes = append(prefixes, rule.Prefix())
|
||||
}
|
||||
commonPrefix := lcp(prefixes)
|
||||
|
||||
// List all objects and calculate lifecycle action based on object name & object modtime
|
||||
marker := ""
|
||||
// Allocate new results channel to receive ObjectInfo.
|
||||
objInfoCh := make(chan ObjectInfo)
|
||||
|
||||
// Walk through all objects
|
||||
if err := objAPI.Walk(ctx, bucket.Name, commonPrefix, objInfoCh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
res, err := objAPI.ListObjects(ctx, bucket.Name, commonPrefix, marker, "", maxObjectList)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var objects []string
|
||||
for _, obj := range res.Objects {
|
||||
for obj := range objInfoCh {
|
||||
if len(objects) == maxObjectList {
|
||||
// Reached maximum delete requests, attempt a delete for now.
|
||||
break
|
||||
}
|
||||
|
||||
// Find the action that need to be executed
|
||||
action := l.ComputeAction(obj.Name, obj.ModTime)
|
||||
switch action {
|
||||
case lifecycle.DeleteAction:
|
||||
if l.ComputeAction(obj.Name, obj.UserTags, obj.ModTime) == lifecycle.DeleteAction {
|
||||
objects = append(objects, obj.Name)
|
||||
default:
|
||||
// Do nothing, for now.
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do.
|
||||
if len(objects) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
waitForLowHTTPReq(int32(globalEndpoints.NEndpoints()))
|
||||
|
||||
// Deletes a list of objects.
|
||||
deleteErrs, err := objAPI.DeleteObjects(ctx, bucket.Name, objects)
|
||||
if err != nil {
|
||||
@@ -173,12 +120,6 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !res.IsTruncated {
|
||||
// We are done here, proceed to next bucket.
|
||||
break
|
||||
}
|
||||
marker = res.NextMarker
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
const dataUsageHashLen = 8
|
||||
|
||||
//go:generate msgp -file $GOFILE -unexported
|
||||
|
||||
// dataUsageHash is the hash type used.
|
||||
type dataUsageHash uint64
|
||||
|
||||
// sizeHistogram is a size histogram.
|
||||
type sizeHistogram [dataUsageBucketLen]uint64
|
||||
|
||||
//msgp:tuple dataUsageEntry
|
||||
type dataUsageEntry struct {
|
||||
// These fields do no include any children.
|
||||
Size int64
|
||||
Objects uint64
|
||||
ObjSizes sizeHistogram
|
||||
|
||||
Children dataUsageHashMap
|
||||
}
|
||||
|
||||
//msgp:ignore dataUsageEntryInfo
|
||||
type dataUsageEntryInfo struct {
|
||||
Name string
|
||||
Parent string
|
||||
Entry dataUsageEntry
|
||||
}
|
||||
|
||||
type dataUsageCacheInfo struct {
|
||||
// Name of the bucket. Also root element.
|
||||
Name string
|
||||
LastUpdate time.Time
|
||||
NextCycle uint8
|
||||
}
|
||||
|
||||
// merge other data usage entry into this, excluding children.
|
||||
func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
||||
e.Objects += other.Objects
|
||||
e.Size += other.Size
|
||||
for i, v := range other.ObjSizes[:] {
|
||||
e.ObjSizes[i] += v
|
||||
}
|
||||
}
|
||||
|
||||
// mod returns true if the hash mod cycles == cycle.
|
||||
func (h dataUsageHash) mod(cycle uint8, cycles uint8) bool {
|
||||
return uint8(h)%cycles == cycle%cycles
|
||||
}
|
||||
|
||||
// addChildString will add a child based on its name.
|
||||
// If it already exists it will not be added again.
|
||||
func (e *dataUsageEntry) addChildString(name string) {
|
||||
e.addChild(hashPath(name))
|
||||
}
|
||||
|
||||
// addChild will add a child based on its hash.
|
||||
// If it already exists it will not be added again.
|
||||
func (e *dataUsageEntry) addChild(hash dataUsageHash) {
|
||||
if _, ok := e.Children[hash]; ok {
|
||||
return
|
||||
}
|
||||
if e.Children == nil {
|
||||
e.Children = make(dataUsageHashMap, 1)
|
||||
}
|
||||
e.Children[hash] = struct{}{}
|
||||
}
|
||||
|
||||
// find a path in the cache.
|
||||
// Returns nil if not found.
|
||||
func (d *dataUsageCache) find(path string) *dataUsageEntry {
|
||||
due, ok := d.Cache[hashPath(path)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &due
|
||||
}
|
||||
|
||||
// dui converts the flattened version of the path to DataUsageInfo.
|
||||
func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
|
||||
e := d.find(path)
|
||||
if e == nil {
|
||||
return DataUsageInfo{LastUpdate: UTCNow()}
|
||||
}
|
||||
flat := d.flatten(*e)
|
||||
return DataUsageInfo{
|
||||
LastUpdate: d.Info.LastUpdate,
|
||||
ObjectsCount: flat.Objects,
|
||||
ObjectsTotalSize: uint64(flat.Size),
|
||||
ObjectsSizesHistogram: flat.ObjSizes.asMap(),
|
||||
BucketsCount: uint64(len(e.Children)),
|
||||
BucketsSizes: d.pathSizes(buckets),
|
||||
}
|
||||
}
|
||||
|
||||
// replace will add or replace an entry in the cache.
|
||||
// If a parent is specified it will be added to that if not already there.
|
||||
// If the parent does not exist, it will be added.
|
||||
func (d *dataUsageCache) replace(path, parent string, e dataUsageEntry) {
|
||||
hash := hashPath(path)
|
||||
if d.Cache == nil {
|
||||
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||
}
|
||||
d.Cache[hash] = e
|
||||
if parent != "" {
|
||||
phash := hashPath(parent)
|
||||
p := d.Cache[phash]
|
||||
p.addChild(hash)
|
||||
d.Cache[phash] = p
|
||||
}
|
||||
}
|
||||
|
||||
// replaceHashed add or replaces an entry to the cache based on its hash.
|
||||
// If a parent is specified it will be added to that if not already there.
|
||||
// If the parent does not exist, it will be added.
|
||||
func (d *dataUsageCache) replaceHashed(hash dataUsageHash, parent *dataUsageHash, e dataUsageEntry) {
|
||||
if d.Cache == nil {
|
||||
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||
}
|
||||
d.Cache[hash] = e
|
||||
if parent != nil {
|
||||
p := d.Cache[*parent]
|
||||
p.addChild(hash)
|
||||
d.Cache[*parent] = p
|
||||
}
|
||||
}
|
||||
|
||||
// StringAll returns a detailed string representation of all entries in the cache.
|
||||
func (d *dataUsageCache) StringAll() string {
|
||||
s := fmt.Sprintf("info:%+v\n", d.Info)
|
||||
for k, v := range d.Cache {
|
||||
s += fmt.Sprintf("\t%v: %+v\n", k, v)
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// String returns a human readable representation of the string.
|
||||
func (h dataUsageHash) String() string {
|
||||
return fmt.Sprintf("%x", uint64(h))
|
||||
}
|
||||
|
||||
// flatten all children of the root into the root element and return it.
|
||||
func (d *dataUsageCache) flatten(root dataUsageEntry) dataUsageEntry {
|
||||
for id := range root.Children {
|
||||
e := d.Cache[id]
|
||||
if len(e.Children) > 0 {
|
||||
e = d.flatten(e)
|
||||
}
|
||||
root.merge(e)
|
||||
}
|
||||
root.Children = nil
|
||||
return root
|
||||
}
|
||||
|
||||
// add a size to the histogram.
|
||||
func (h *sizeHistogram) add(size int64) {
|
||||
// Fetch the histogram interval corresponding
|
||||
// to the passed object size.
|
||||
for i, interval := range ObjectsHistogramIntervals {
|
||||
if size >= interval.start && size <= interval.end {
|
||||
h[i]++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// asMap returns the map as a map[string]uint64.
|
||||
func (h *sizeHistogram) asMap() map[string]uint64 {
|
||||
res := make(map[string]uint64, 7)
|
||||
for i, count := range h {
|
||||
res[ObjectsHistogramIntervals[i].name] = count
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// pathSizes returns the path sizes as a map.
|
||||
func (d *dataUsageCache) pathSizes(buckets []BucketInfo) map[string]uint64 {
|
||||
var dst = make(map[string]uint64, len(buckets))
|
||||
for _, bucket := range buckets {
|
||||
e := d.find(bucket.Name)
|
||||
if e == nil {
|
||||
continue
|
||||
}
|
||||
flat := d.flatten(*e)
|
||||
dst[bucket.Name] = uint64(flat.Size)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// sizeRecursive returns the path as a flattened entry.
|
||||
func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
|
||||
root := d.find(path)
|
||||
if root == nil || len(root.Children) == 0 {
|
||||
return root
|
||||
}
|
||||
flat := d.flatten(*root)
|
||||
return &flat
|
||||
}
|
||||
|
||||
// dataUsageCache contains a cache of data usage entries.
|
||||
//msgp:ignore dataUsageCache
|
||||
type dataUsageCache struct {
|
||||
Info dataUsageCacheInfo
|
||||
Cache map[dataUsageHash]dataUsageEntry
|
||||
}
|
||||
|
||||
// root returns the root of the cache.
|
||||
func (d *dataUsageCache) root() *dataUsageEntry {
|
||||
return d.find(d.Info.Name)
|
||||
}
|
||||
|
||||
// rootHash returns the root of the cache.
|
||||
func (d *dataUsageCache) rootHash() dataUsageHash {
|
||||
return hashPath(d.Info.Name)
|
||||
}
|
||||
|
||||
// clone returns a copy of the cache with no references to the existing.
|
||||
func (d *dataUsageCache) clone() dataUsageCache {
|
||||
clone := dataUsageCache{
|
||||
Info: d.Info,
|
||||
Cache: make(map[dataUsageHash]dataUsageEntry, len(d.Cache)),
|
||||
}
|
||||
for k, v := range d.Cache {
|
||||
clone.Cache[k] = v
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// merge root of other into d.
|
||||
// children of root will be flattened before being merged.
|
||||
// Last update time will be set to the last updated.
|
||||
func (d *dataUsageCache) merge(other dataUsageCache) {
|
||||
existingRoot := d.root()
|
||||
otherRoot := other.root()
|
||||
if existingRoot == nil && otherRoot == nil {
|
||||
return
|
||||
}
|
||||
if otherRoot == nil {
|
||||
return
|
||||
}
|
||||
if existingRoot == nil {
|
||||
*d = other.clone()
|
||||
return
|
||||
}
|
||||
if other.Info.LastUpdate.After(d.Info.LastUpdate) {
|
||||
d.Info.LastUpdate = other.Info.LastUpdate
|
||||
}
|
||||
existingRoot.merge(*otherRoot)
|
||||
eHash := d.rootHash()
|
||||
for key := range otherRoot.Children {
|
||||
entry := other.Cache[key]
|
||||
flat := other.flatten(entry)
|
||||
existing := d.Cache[key]
|
||||
// If not found, merging simply adds.
|
||||
existing.merge(flat)
|
||||
d.replaceHashed(key, &eHash, existing)
|
||||
}
|
||||
}
|
||||
|
||||
// load the cache content with name from minioMetaBackgroundOpsBucket.
|
||||
// Only backend errors are returned as errors.
|
||||
// If the object is not found or unable to deserialize d is cleared and nil error is returned.
|
||||
func (d *dataUsageCache) load(ctx context.Context, store ObjectLayer, name string) error {
|
||||
var buf bytes.Buffer
|
||||
err := store.GetObject(ctx, dataUsageBucket, name, 0, -1, &buf, "", ObjectOptions{})
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) {
|
||||
return toObjectErr(err, dataUsageBucket, name)
|
||||
}
|
||||
*d = dataUsageCache{}
|
||||
return nil
|
||||
}
|
||||
err = d.deserialize(buf.Bytes())
|
||||
if err != nil {
|
||||
*d = dataUsageCache{}
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// save the content of the cache to minioMetaBackgroundOpsBucket with the provided name.
|
||||
func (d *dataUsageCache) save(ctx context.Context, store ObjectLayer, name string) error {
|
||||
b := d.serialize()
|
||||
size := int64(len(b))
|
||||
r, err := hash.NewReader(bytes.NewReader(b), size, "", "", size, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = store.PutObject(ctx,
|
||||
dataUsageBucket,
|
||||
name,
|
||||
NewPutObjReader(r, nil, nil),
|
||||
ObjectOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// dataUsageCacheVer indicates the cache version.
|
||||
// Bumping the cache version will drop data from previous versions
|
||||
// and write new data with the new version.
|
||||
const dataUsageCacheVer = 1
|
||||
|
||||
// serialize the contents of the cache.
|
||||
func (d *dataUsageCache) serialize() []byte {
|
||||
// Alloc pessimistically
|
||||
// dataUsageCacheVer
|
||||
due := dataUsageEntry{}
|
||||
msgLen := 1
|
||||
msgLen += d.Info.Msgsize()
|
||||
// len(d.Cache)
|
||||
msgLen += binary.MaxVarintLen64
|
||||
// Hashes (one for key, assume 1 child/node)
|
||||
msgLen += len(d.Cache) * dataUsageHashLen * 2
|
||||
msgLen += len(d.Cache) * due.Msgsize()
|
||||
|
||||
// Create destination buffer...
|
||||
dst := make([]byte, 0, msgLen)
|
||||
|
||||
var n int
|
||||
tmp := make([]byte, 1024)
|
||||
// byte: version.
|
||||
dst = append(dst, dataUsageCacheVer)
|
||||
// Info...
|
||||
dst, err := d.Info.MarshalMsg(dst)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
n = binary.PutUvarint(tmp, uint64(len(d.Cache)))
|
||||
dst = append(dst, tmp[:n]...)
|
||||
|
||||
for k, v := range d.Cache {
|
||||
// Put key
|
||||
binary.LittleEndian.PutUint64(tmp[:dataUsageHashLen], uint64(k))
|
||||
dst = append(dst, tmp[:8]...)
|
||||
tmp, err = v.MarshalMsg(tmp[:0])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// key, value pairs.
|
||||
dst = append(dst, tmp...)
|
||||
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// deserialize the supplied byte slice into the cache.
|
||||
func (d *dataUsageCache) deserialize(b []byte) error {
|
||||
if len(b) < 1 {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
switch b[0] {
|
||||
case 1:
|
||||
default:
|
||||
return fmt.Errorf("dataUsageCache: unknown version: %d", int(b[0]))
|
||||
}
|
||||
b = b[1:]
|
||||
|
||||
// Info...
|
||||
b, err := d.Info.UnmarshalMsg(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheLen, n := binary.Uvarint(b)
|
||||
if n <= 0 {
|
||||
return fmt.Errorf("dataUsageCache: reading cachelen, n <= 0 ")
|
||||
}
|
||||
b = b[n:]
|
||||
d.Cache = make(map[dataUsageHash]dataUsageEntry, cacheLen)
|
||||
|
||||
for i := 0; i < int(cacheLen); i++ {
|
||||
if len(b) <= dataUsageHashLen {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
k := binary.LittleEndian.Uint64(b[:dataUsageHashLen])
|
||||
b = b[dataUsageHashLen:]
|
||||
var v dataUsageEntry
|
||||
b, err = v.UnmarshalMsg(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Cache[dataUsageHash(k)] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trim this from start+end of hashes.
|
||||
var hashPathCutSet = dataUsageRoot
|
||||
|
||||
func init() {
|
||||
if dataUsageRoot != string(filepath.Separator) {
|
||||
hashPathCutSet = dataUsageRoot + string(filepath.Separator)
|
||||
}
|
||||
}
|
||||
|
||||
// hashPath calculates a hash of the provided string.
|
||||
func hashPath(data string) dataUsageHash {
|
||||
if data != dataUsageRoot {
|
||||
data = strings.Trim(data, hashPathCutSet)
|
||||
}
|
||||
data = path.Clean(data)
|
||||
return dataUsageHash(xxhash.Sum64String(data))
|
||||
}
|
||||
|
||||
//msgp:ignore dataUsageEntryInfo
|
||||
type dataUsageHashMap map[dataUsageHash]struct{}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (d dataUsageHashMap) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, d.Msgsize())
|
||||
|
||||
// Write bin header manually
|
||||
const mbin32 uint8 = 0xc6
|
||||
sz := uint32(len(d)) * dataUsageHashLen
|
||||
o = append(o, mbin32, byte(sz>>24), byte(sz>>16), byte(sz>>8), byte(sz))
|
||||
|
||||
var tmp [dataUsageHashLen]byte
|
||||
for k := range d {
|
||||
binary.LittleEndian.PutUint64(tmp[:], uint64(k))
|
||||
o = append(o, tmp[:]...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (d dataUsageHashMap) Msgsize() (s int) {
|
||||
s = 5 + len(d)*dataUsageHashLen
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (d *dataUsageHashMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var hashes []byte
|
||||
hashes, bts, err = msgp.ReadBytesZC(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "dataUsageHashMap")
|
||||
return
|
||||
}
|
||||
|
||||
var dst = make(dataUsageHashMap, len(hashes)/dataUsageHashLen)
|
||||
for len(hashes) >= dataUsageHashLen {
|
||||
dst[dataUsageHash(binary.LittleEndian.Uint64(hashes[:dataUsageHashLen]))] = struct{}{}
|
||||
hashes = hashes[dataUsageHashLen:]
|
||||
}
|
||||
*d = dst
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
func (d *dataUsageHashMap) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, err = dc.ReadBytesHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
var dst = make(dataUsageHashMap, zb0001)
|
||||
var tmp [8]byte
|
||||
for i := uint32(0); i < zb0001; i++ {
|
||||
_, err = io.ReadFull(dc, tmp[:])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "dataUsageHashMap")
|
||||
return
|
||||
}
|
||||
dst[dataUsageHash(binary.LittleEndian.Uint64(tmp[:]))] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (d dataUsageHashMap) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = en.WriteBytesHeader(uint32(len(d)) * dataUsageHashLen)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
var tmp [dataUsageHashLen]byte
|
||||
for k := range d {
|
||||
binary.LittleEndian.PutUint64(tmp[:], uint64(k))
|
||||
_, err = en.Write(tmp[:])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
package cmd
|
||||
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var field []byte
|
||||
_ = field
|
||||
var zb0001 uint32
|
||||
zb0001, err = dc.ReadMapHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
for zb0001 > 0 {
|
||||
zb0001--
|
||||
field, err = dc.ReadMapKeyPtr()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
switch msgp.UnsafeString(field) {
|
||||
case "Name":
|
||||
z.Name, err = dc.ReadString()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Name")
|
||||
return
|
||||
}
|
||||
case "LastUpdate":
|
||||
z.LastUpdate, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "NextCycle":
|
||||
z.NextCycle, err = dc.ReadUint8()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 3
|
||||
// write "Name"
|
||||
err = en.Append(0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteString(z.Name)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Name")
|
||||
return
|
||||
}
|
||||
// write "LastUpdate"
|
||||
err = en.Append(0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z.LastUpdate)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
// write "NextCycle"
|
||||
err = en.Append(0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint8(z.NextCycle)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 3
|
||||
// string "Name"
|
||||
o = append(o, 0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
o = msgp.AppendString(o, z.Name)
|
||||
// string "LastUpdate"
|
||||
o = append(o, 0xaa, 0x4c, 0x61, 0x73, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65)
|
||||
o = msgp.AppendTime(o, z.LastUpdate)
|
||||
// string "NextCycle"
|
||||
o = append(o, 0xa9, 0x4e, 0x65, 0x78, 0x74, 0x43, 0x79, 0x63, 0x6c, 0x65)
|
||||
o = msgp.AppendUint8(o, z.NextCycle)
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var field []byte
|
||||
_ = field
|
||||
var zb0001 uint32
|
||||
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
for zb0001 > 0 {
|
||||
zb0001--
|
||||
field, bts, err = msgp.ReadMapKeyZC(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
switch msgp.UnsafeString(field) {
|
||||
case "Name":
|
||||
z.Name, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Name")
|
||||
return
|
||||
}
|
||||
case "LastUpdate":
|
||||
z.LastUpdate, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LastUpdate")
|
||||
return
|
||||
}
|
||||
case "NextCycle":
|
||||
z.NextCycle, bts, err = msgp.ReadUint8Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z dataUsageCacheInfo) Msgsize() (s int) {
|
||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 11 + msgp.TimeSize + 10 + msgp.Uint8Size
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *dataUsageEntry) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, err = dc.ReadArrayHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 4 {
|
||||
err = msgp.ArrayError{Wanted: 4, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Size, err = dc.ReadInt64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Size")
|
||||
return
|
||||
}
|
||||
z.Objects, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Objects")
|
||||
return
|
||||
}
|
||||
var zb0002 uint32
|
||||
zb0002, err = dc.ReadArrayHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes")
|
||||
return
|
||||
}
|
||||
if zb0002 != uint32(dataUsageBucketLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
|
||||
return
|
||||
}
|
||||
for za0001 := range z.ObjSizes {
|
||||
z.ObjSizes[za0001], err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
err = z.Children.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Children")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *dataUsageEntry) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// array header, size 4
|
||||
err = en.Append(0x94)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteInt64(z.Size)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Size")
|
||||
return
|
||||
}
|
||||
err = en.WriteUint64(z.Objects)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Objects")
|
||||
return
|
||||
}
|
||||
err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes")
|
||||
return
|
||||
}
|
||||
for za0001 := range z.ObjSizes {
|
||||
err = en.WriteUint64(z.ObjSizes[za0001])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
err = z.Children.EncodeMsg(en)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Children")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *dataUsageEntry) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// array header, size 4
|
||||
o = append(o, 0x94)
|
||||
o = msgp.AppendInt64(o, z.Size)
|
||||
o = msgp.AppendUint64(o, z.Objects)
|
||||
o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
|
||||
for za0001 := range z.ObjSizes {
|
||||
o = msgp.AppendUint64(o, z.ObjSizes[za0001])
|
||||
}
|
||||
o, err = z.Children.MarshalMsg(o)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Children")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *dataUsageEntry) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != 4 {
|
||||
err = msgp.ArrayError{Wanted: 4, Got: zb0001}
|
||||
return
|
||||
}
|
||||
z.Size, bts, err = msgp.ReadInt64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Size")
|
||||
return
|
||||
}
|
||||
z.Objects, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Objects")
|
||||
return
|
||||
}
|
||||
var zb0002 uint32
|
||||
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes")
|
||||
return
|
||||
}
|
||||
if zb0002 != uint32(dataUsageBucketLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0002}
|
||||
return
|
||||
}
|
||||
for za0001 := range z.ObjSizes {
|
||||
z.ObjSizes[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "ObjSizes", za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
bts, err = z.Children.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Children")
|
||||
return
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *dataUsageEntry) Msgsize() (s int) {
|
||||
s = 1 + msgp.Int64Size + msgp.Uint64Size + msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size)) + z.Children.Msgsize()
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *dataUsageHash) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
{
|
||||
var zb0001 uint64
|
||||
zb0001, err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
(*z) = dataUsageHash(zb0001)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z dataUsageHash) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = en.WriteUint64(uint64(z))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z dataUsageHash) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
o = msgp.AppendUint64(o, uint64(z))
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *dataUsageHash) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
{
|
||||
var zb0001 uint64
|
||||
zb0001, bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
(*z) = dataUsageHash(zb0001)
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z dataUsageHash) Msgsize() (s int) {
|
||||
s = msgp.Uint64Size
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *sizeHistogram) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, err = dc.ReadArrayHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != uint32(dataUsageBucketLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
z[za0001], err = dc.ReadUint64()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *sizeHistogram) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = en.WriteArrayHeader(uint32(dataUsageBucketLen))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
err = en.WriteUint64(z[za0001])
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *sizeHistogram) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
o = msgp.AppendArrayHeader(o, uint32(dataUsageBucketLen))
|
||||
for za0001 := range z {
|
||||
o = msgp.AppendUint64(o, z[za0001])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *sizeHistogram) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var zb0001 uint32
|
||||
zb0001, bts, err = msgp.ReadArrayHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if zb0001 != uint32(dataUsageBucketLen) {
|
||||
err = msgp.ArrayError{Wanted: uint32(dataUsageBucketLen), Got: zb0001}
|
||||
return
|
||||
}
|
||||
for za0001 := range z {
|
||||
z[za0001], bts, err = msgp.ReadUint64Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, za0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *sizeHistogram) Msgsize() (s int) {
|
||||
s = msgp.ArrayHeaderSize + (dataUsageBucketLen * (msgp.Uint64Size))
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package cmd
|
||||
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshaldataUsageCacheInfo(t *testing.T) {
|
||||
v := dataUsageCacheInfo{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgdataUsageCacheInfo(b *testing.B) {
|
||||
v := dataUsageCacheInfo{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgdataUsageCacheInfo(b *testing.B) {
|
||||
v := dataUsageCacheInfo{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshaldataUsageCacheInfo(b *testing.B) {
|
||||
v := dataUsageCacheInfo{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodedataUsageCacheInfo(t *testing.T) {
|
||||
v := dataUsageCacheInfo{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodedataUsageCacheInfo Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := dataUsageCacheInfo{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodedataUsageCacheInfo(b *testing.B) {
|
||||
v := dataUsageCacheInfo{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodedataUsageCacheInfo(b *testing.B) {
|
||||
v := dataUsageCacheInfo{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshaldataUsageEntry(t *testing.T) {
|
||||
v := dataUsageEntry{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgdataUsageEntry(b *testing.B) {
|
||||
v := dataUsageEntry{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgdataUsageEntry(b *testing.B) {
|
||||
v := dataUsageEntry{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshaldataUsageEntry(b *testing.B) {
|
||||
v := dataUsageEntry{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodedataUsageEntry(t *testing.T) {
|
||||
v := dataUsageEntry{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodedataUsageEntry Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := dataUsageEntry{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodedataUsageEntry(b *testing.B) {
|
||||
v := dataUsageEntry{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodedataUsageEntry(b *testing.B) {
|
||||
v := dataUsageEntry{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
|
||||
v := sizeHistogram{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
left, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left)
|
||||
}
|
||||
|
||||
left, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(left) > 0 {
|
||||
t.Errorf("%d bytes left over after Skip(): %q", len(left), left)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMarshalMsgsizeHistogram(b *testing.B) {
|
||||
v := sizeHistogram{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgsizeHistogram(b *testing.B) {
|
||||
v := sizeHistogram{}
|
||||
bts := make([]byte, 0, v.Msgsize())
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bts, _ = v.MarshalMsg(bts[0:0])
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalsizeHistogram(b *testing.B) {
|
||||
v := sizeHistogram{}
|
||||
bts, _ := v.MarshalMsg(nil)
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(int64(len(bts)))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := v.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeDecodesizeHistogram(t *testing.T) {
|
||||
v := sizeHistogram{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodesizeHistogram Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := sizeHistogram{}
|
||||
err := msgp.Decode(&buf, &vn)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
msgp.Encode(&buf, &v)
|
||||
err = msgp.NewReader(&buf).Skip()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEncodesizeHistogram(b *testing.B) {
|
||||
v := sizeHistogram{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
en := msgp.NewWriter(msgp.Nowhere)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.EncodeMsg(en)
|
||||
}
|
||||
en.Flush()
|
||||
}
|
||||
|
||||
func BenchmarkDecodesizeHistogram(b *testing.B) {
|
||||
v := sizeHistogram{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+370
-80
@@ -20,117 +20,86 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/color"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
const (
|
||||
dataUsageObjName = "data-usage"
|
||||
dataUsageCrawlInterval = 12 * time.Hour
|
||||
dataUsageObjName = ".usage.json"
|
||||
dataUsageCacheName = ".usage-cache.bin"
|
||||
envDataUsageCrawlConf = "MINIO_DISK_USAGE_CRAWL_ENABLE"
|
||||
envDataUsageCrawlDelay = "MINIO_DISK_USAGE_CRAWL_DELAY"
|
||||
envDataUsageCrawlDebug = "MINIO_DISK_USAGE_CRAWL_DEBUG"
|
||||
dataUsageSleepPerFolder = 1 * time.Millisecond
|
||||
dataUsageSleepDefMult = 10.0
|
||||
dataUsageUpdateDirCycles = 16
|
||||
dataUsageRoot = SlashSeparator
|
||||
dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
|
||||
dataUsageStartDelay = 5 * time.Minute // Time to wait on startup and between cycles.
|
||||
)
|
||||
|
||||
func initDataUsageStats() {
|
||||
go runDataUsageInfoUpdateRoutine()
|
||||
}
|
||||
|
||||
func runDataUsageInfoUpdateRoutine() {
|
||||
// Wait until the object layer is ready
|
||||
var objAPI ObjectLayer
|
||||
for {
|
||||
objAPI = newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch v := objAPI.(type) {
|
||||
case *xlZones:
|
||||
runDataUsageInfoForXLZones(ctx, v, GlobalServiceDoneCh)
|
||||
case *FSObjects:
|
||||
runDataUsageInfoForFS(ctx, v, GlobalServiceDoneCh)
|
||||
default:
|
||||
return
|
||||
// initDataUsageStats will start the crawler unless disabled.
|
||||
func initDataUsageStats(ctx context.Context, objAPI ObjectLayer) {
|
||||
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn {
|
||||
go runDataUsageInfo(ctx, objAPI)
|
||||
}
|
||||
}
|
||||
|
||||
func runDataUsageInfoForFS(ctx context.Context, fsObj *FSObjects, endCh <-chan struct{}) {
|
||||
t := time.NewTicker(dataUsageCrawlInterval)
|
||||
defer t.Stop()
|
||||
func runDataUsageInfo(ctx context.Context, objAPI ObjectLayer) {
|
||||
for {
|
||||
// Get data usage info of the FS Object
|
||||
usageInfo := fsObj.crawlAndGetDataUsageInfo(ctx, endCh)
|
||||
// Save the data usage in the disk
|
||||
err := storeDataUsageInBackend(ctx, fsObj, usageInfo)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.NewTimer(dataUsageStartDelay).C:
|
||||
// Wait before starting next cycle and wait on startup.
|
||||
results := make(chan DataUsageInfo, 1)
|
||||
go storeDataUsageInBackend(ctx, objAPI, results)
|
||||
err := objAPI.CrawlAndGetDataUsage(ctx, results)
|
||||
close(results)
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
select {
|
||||
case <-endCh:
|
||||
return
|
||||
// Wait until the next crawl interval
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runDataUsageInfoForXLZones(ctx context.Context, z *xlZones, endCh <-chan struct{}) {
|
||||
locker := z.NewNSLock(ctx, minioMetaBucket, "leader-data-usage-info")
|
||||
for {
|
||||
err := locker.GetLock(newDynamicTimeout(time.Millisecond, time.Millisecond))
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Minute)
|
||||
continue
|
||||
}
|
||||
// Break without locking
|
||||
break
|
||||
}
|
||||
|
||||
t := time.NewTicker(dataUsageCrawlInterval)
|
||||
defer t.Stop()
|
||||
for {
|
||||
usageInfo := z.crawlAndGetDataUsage(ctx, endCh)
|
||||
err := storeDataUsageInBackend(ctx, z, usageInfo)
|
||||
// storeDataUsageInBackend will store all objects sent on the gui channel until closed.
|
||||
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, gui <-chan DataUsageInfo) {
|
||||
for dataUsageInfo := range gui {
|
||||
dataUsageJSON, err := json.Marshal(dataUsageInfo)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-endCh:
|
||||
locker.Unlock()
|
||||
return
|
||||
case <-t.C:
|
||||
size := int64(len(dataUsageJSON))
|
||||
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size, false)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, dataUsageInfo DataUsageInfo) error {
|
||||
dataUsageJSON, err := json.Marshal(dataUsageInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
size := int64(len(dataUsageJSON))
|
||||
r, err := hash.NewReader(bytes.NewReader(dataUsageJSON), size, "", "", size, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = objAPI.PutObject(ctx, minioMetaBackgroundOpsBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsageInfo, error) {
|
||||
var dataUsageInfoJSON bytes.Buffer
|
||||
|
||||
err := objAPI.GetObject(ctx, minioMetaBackgroundOpsBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
|
||||
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
|
||||
if err != nil {
|
||||
return DataUsageInfo{}, nil
|
||||
if isErrObjectNotFound(err) {
|
||||
return DataUsageInfo{}, nil
|
||||
}
|
||||
return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName)
|
||||
}
|
||||
|
||||
var dataUsageInfo DataUsageInfo
|
||||
@@ -142,3 +111,324 @@ func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsag
|
||||
|
||||
return dataUsageInfo, nil
|
||||
}
|
||||
|
||||
// Item represents each file while walking.
|
||||
type Item struct {
|
||||
Path string
|
||||
Typ os.FileMode
|
||||
}
|
||||
|
||||
type getSizeFn func(item Item) (int64, error)
|
||||
|
||||
type cachedFolder struct {
|
||||
name string
|
||||
parent *dataUsageHash
|
||||
}
|
||||
|
||||
type folderScanner struct {
|
||||
root string
|
||||
getSize getSizeFn
|
||||
oldCache dataUsageCache
|
||||
newCache dataUsageCache
|
||||
waitForLowActiveIO func()
|
||||
|
||||
dataUsageCrawlMult float64
|
||||
dataUsageCrawlDebug bool
|
||||
|
||||
newFolders []cachedFolder
|
||||
existingFolders []cachedFolder
|
||||
}
|
||||
|
||||
// sleepDuration multiplies the duration d by x and sleeps if is more than 100 micro seconds.
|
||||
// sleep is limited to max 1 second.
|
||||
func sleepDuration(d time.Duration, x float64) {
|
||||
// Don't sleep for really small amount of time
|
||||
if d := time.Duration(float64(d) * x); d > time.Microsecond*100 {
|
||||
if d > time.Second {
|
||||
d = time.Second
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
}
|
||||
|
||||
// scanQueuedLevels will scan the provided folders.
|
||||
// Files found in the folders will be added to f.newCache.
|
||||
// If final is provided folders will be put into f.newFolders or f.existingFolders.
|
||||
// If final is not provided the folders found are returned from the function.
|
||||
func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFolder, final bool) ([]cachedFolder, error) {
|
||||
var nextFolders []cachedFolder
|
||||
done := ctx.Done()
|
||||
for _, folder := range folders {
|
||||
select {
|
||||
case <-done:
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
f.waitForLowActiveIO()
|
||||
sleepDuration(dataUsageSleepPerFolder, f.dataUsageCrawlMult)
|
||||
|
||||
cache := dataUsageEntry{}
|
||||
thisHash := hashPath(folder.name)
|
||||
|
||||
err := readDirFn(path.Join(f.root, folder.name), func(entName string, typ os.FileMode) error {
|
||||
// Parse
|
||||
entName = path.Clean(path.Join(folder.name, entName))
|
||||
bucket, _ := path2BucketObjectWithBasePath(f.root, entName)
|
||||
if bucket == "" {
|
||||
if f.dataUsageCrawlDebug {
|
||||
logger.Info(color.Green("data-usage:")+" no bucket (%s,%s)", f.root, entName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if isReservedOrInvalidBucket(bucket, false) {
|
||||
if f.dataUsageCrawlDebug {
|
||||
logger.Info(color.Green("data-usage:")+" invalid bucket: %v, entry: %v", bucket, entName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if typ&os.ModeDir != 0 {
|
||||
h := hashPath(entName)
|
||||
_, exists := f.oldCache.Cache[h]
|
||||
cache.addChildString(entName)
|
||||
|
||||
this := cachedFolder{name: entName, parent: &thisHash}
|
||||
cache.addChild(h)
|
||||
if final {
|
||||
if exists {
|
||||
f.existingFolders = append(f.existingFolders, this)
|
||||
} else {
|
||||
f.newFolders = append(f.newFolders, this)
|
||||
}
|
||||
} else {
|
||||
nextFolders = append(nextFolders, this)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
f.waitForLowActiveIO()
|
||||
// Dynamic time delay.
|
||||
t := UTCNow()
|
||||
|
||||
// Get file size, ignore errors.
|
||||
size, err := f.getSize(Item{Path: path.Join(f.root, entName), Typ: typ})
|
||||
|
||||
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
||||
if err == errSkipFile {
|
||||
return nil
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
cache.Size += size
|
||||
cache.Objects++
|
||||
cache.ObjSizes.add(size)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.newCache.replaceHashed(thisHash, folder.parent, cache)
|
||||
}
|
||||
return nextFolders, nil
|
||||
}
|
||||
|
||||
// deepScanFolder will deep scan a folder and return the size if no error occurs.
|
||||
func (f *folderScanner) deepScanFolder(ctx context.Context, folder string) (*dataUsageEntry, error) {
|
||||
var cache dataUsageEntry
|
||||
|
||||
done := ctx.Done()
|
||||
|
||||
var addDir func(entName string, typ os.FileMode) error
|
||||
var dirStack = []string{f.root, folder}
|
||||
|
||||
addDir = func(entName string, typ os.FileMode) error {
|
||||
select {
|
||||
case <-done:
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
f.waitForLowActiveIO()
|
||||
if typ&os.ModeDir != 0 {
|
||||
dirStack = append(dirStack, entName)
|
||||
err := readDirFn(path.Join(dirStack...), addDir)
|
||||
dirStack = dirStack[:len(dirStack)-1]
|
||||
sleepDuration(dataUsageSleepPerFolder, f.dataUsageCrawlMult)
|
||||
return err
|
||||
}
|
||||
|
||||
// Dynamic time delay.
|
||||
t := UTCNow()
|
||||
|
||||
// Get file size, ignore errors.
|
||||
dirStack = append(dirStack, entName)
|
||||
fileName := path.Join(dirStack...)
|
||||
dirStack = dirStack[:len(dirStack)-1]
|
||||
|
||||
size, err := f.getSize(Item{Path: fileName, Typ: typ})
|
||||
|
||||
// Don't sleep for really small amount of time
|
||||
sleepDuration(time.Since(t), f.dataUsageCrawlMult)
|
||||
|
||||
if err == errSkipFile {
|
||||
return nil
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
cache.Size += size
|
||||
cache.Objects++
|
||||
cache.ObjSizes.add(size)
|
||||
return nil
|
||||
}
|
||||
err := readDirFn(path.Join(dirStack...), addDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cache, nil
|
||||
}
|
||||
|
||||
// updateUsage will crawl the basepath+cache.Info.Name and return an updated cache.
|
||||
// The returned cache will always be valid, but may not be updated from the existing.
|
||||
// Before each operation waitForLowActiveIO is called which can be used to temporarily halt the crawler.
|
||||
// If the supplied context is canceled the function will return at the first chance.
|
||||
func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, waitForLowActiveIO func(), getSize getSizeFn) (dataUsageCache, error) {
|
||||
t := UTCNow()
|
||||
|
||||
dataUsageDebug := env.Get(envDataUsageCrawlDebug, config.EnableOff) == config.EnableOn
|
||||
defer func() {
|
||||
if dataUsageDebug {
|
||||
logger.Info(color.Green("updateUsage")+" Crawl time at %s: %v", basePath, time.Since(t))
|
||||
}
|
||||
}()
|
||||
|
||||
if cache.Info.Name == "" {
|
||||
cache.Info.Name = dataUsageRoot
|
||||
}
|
||||
|
||||
delayMult, err := strconv.ParseFloat(env.Get(envDataUsageCrawlDelay, "10.0"), 64)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
delayMult = dataUsageSleepDefMult
|
||||
}
|
||||
|
||||
s := folderScanner{
|
||||
root: basePath,
|
||||
getSize: getSize,
|
||||
oldCache: cache,
|
||||
newCache: dataUsageCache{Info: cache.Info},
|
||||
waitForLowActiveIO: waitForLowActiveIO,
|
||||
newFolders: nil,
|
||||
existingFolders: nil,
|
||||
dataUsageCrawlMult: delayMult,
|
||||
dataUsageCrawlDebug: dataUsageDebug,
|
||||
}
|
||||
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(color.Green("runDataUsageInfo:") + " Starting crawler master")
|
||||
}
|
||||
|
||||
done := ctx.Done()
|
||||
var flattenLevels = 3
|
||||
|
||||
// If we are scanning inside a bucket reduce depth by 1.
|
||||
if cache.Info.Name != dataUsageRoot {
|
||||
flattenLevels--
|
||||
}
|
||||
|
||||
var logPrefix, logSuffix string
|
||||
if s.dataUsageCrawlDebug {
|
||||
logPrefix = color.Green("data-usage: ")
|
||||
logSuffix = color.Blue(" - %v + %v", basePath, cache.Info.Name)
|
||||
}
|
||||
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"Cycle: %v"+logSuffix, cache.Info.NextCycle)
|
||||
}
|
||||
|
||||
// Always scan flattenLevels deep. Cache root is level 0.
|
||||
todo := []cachedFolder{{name: cache.Info.Name}}
|
||||
for i := 0; i < flattenLevels; i++ {
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"Level %v, scanning %v directories."+logSuffix, i, len(todo))
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
return cache, ctx.Err()
|
||||
default:
|
||||
}
|
||||
var err error
|
||||
todo, err = s.scanQueuedLevels(ctx, todo, i == flattenLevels-1)
|
||||
if err != nil {
|
||||
// No useful information...
|
||||
return cache, err
|
||||
}
|
||||
}
|
||||
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"New folders: %v"+logSuffix, s.newFolders)
|
||||
}
|
||||
|
||||
// Add new folders first
|
||||
for _, folder := range s.newFolders {
|
||||
select {
|
||||
case <-done:
|
||||
return s.newCache, ctx.Err()
|
||||
default:
|
||||
}
|
||||
du, err := s.deepScanFolder(ctx, folder.name)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
if du == nil {
|
||||
logger.LogIf(ctx, errors.New("data-usage: no disk usage provided"))
|
||||
continue
|
||||
}
|
||||
s.newCache.replace(folder.name, "", *du)
|
||||
// Add to parent manually
|
||||
if folder.parent != nil {
|
||||
parent := s.newCache.Cache[*folder.parent]
|
||||
parent.addChildString(folder.name)
|
||||
}
|
||||
}
|
||||
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"Existing folders: %v"+logSuffix, len(s.existingFolders))
|
||||
}
|
||||
|
||||
// Do selective scanning of existing folders.
|
||||
for _, folder := range s.existingFolders {
|
||||
select {
|
||||
case <-done:
|
||||
return s.newCache, ctx.Err()
|
||||
default:
|
||||
}
|
||||
h := hashPath(folder.name)
|
||||
if !h.mod(s.oldCache.Info.NextCycle, dataUsageUpdateDirCycles) {
|
||||
s.newCache.replaceHashed(h, folder.parent, s.oldCache.Cache[h])
|
||||
continue
|
||||
}
|
||||
|
||||
// Update on this cycle...
|
||||
du, err := s.deepScanFolder(ctx, folder.name)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
if du == nil {
|
||||
logger.LogIf(ctx, errors.New("data-usage: no disk usage provided"))
|
||||
continue
|
||||
}
|
||||
s.newCache.replaceHashed(h, folder.parent, *du)
|
||||
}
|
||||
|
||||
s.newCache.Info.LastUpdate = UTCNow()
|
||||
s.newCache.Info.NextCycle++
|
||||
return s.newCache, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,651 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2020 MinIO, Inc.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type usageTestFile struct {
|
||||
name string
|
||||
size int
|
||||
}
|
||||
|
||||
func TestDataUsageUpdate(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUsageUpdate")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
defer os.RemoveAll(base)
|
||||
var files = []usageTestFile{
|
||||
{name: "rootfile", size: 10000},
|
||||
{name: "rootfile2", size: 10000},
|
||||
{name: "dir1/d1file", size: 2000},
|
||||
{name: "dir2/d2file", size: 300},
|
||||
{name: "dir1/dira/dafile", size: 100000},
|
||||
{name: "dir1/dira/dbfile", size: 200000},
|
||||
{name: "dir1/dira/dirasub/dcfile", size: 1000000},
|
||||
{name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||
}
|
||||
createUsageTestFiles(t, base, files)
|
||||
|
||||
getSize := func(item Item) (i int64, err error) {
|
||||
if item.Typ&os.ModeDir == 0 {
|
||||
s, err := os.Stat(item.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.Size(), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
got, err := updateUsage(context.Background(), base, dataUsageCache{}, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test dirs
|
||||
var want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
flatten bool
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
size: 1322310,
|
||||
flatten: true,
|
||||
objs: 8,
|
||||
oSizes: sizeHistogram{0: 2, 1: 6},
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
size: 20000,
|
||||
objs: 2,
|
||||
oSizes: sizeHistogram{1: 2},
|
||||
},
|
||||
{
|
||||
path: "/dir1",
|
||||
size: 2000,
|
||||
objs: 1,
|
||||
oSizes: sizeHistogram{1: 1},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira",
|
||||
flatten: true,
|
||||
size: 1300010,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira/",
|
||||
flatten: true,
|
||||
size: 1300010,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira",
|
||||
size: 300000,
|
||||
objs: 2,
|
||||
oSizes: sizeHistogram{0: 0, 1: 2},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira/",
|
||||
size: 300000,
|
||||
objs: 2,
|
||||
oSizes: sizeHistogram{0: 0, 1: 2},
|
||||
},
|
||||
{
|
||||
path: "/nonexistying",
|
||||
isNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if w.flatten {
|
||||
*e = got.flatten(*e)
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
files = []usageTestFile{
|
||||
{
|
||||
name: "newfolder/afile",
|
||||
size: 4,
|
||||
},
|
||||
{
|
||||
name: "newfolder/anotherone",
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
name: "newfolder/anemptyone",
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
name: "dir1/fileindir1",
|
||||
size: 20000,
|
||||
},
|
||||
{
|
||||
name: "dir1/dirc/fileindirc",
|
||||
size: 20000,
|
||||
},
|
||||
{
|
||||
name: "rootfile3",
|
||||
size: 1000,
|
||||
},
|
||||
}
|
||||
createUsageTestFiles(t, base, files)
|
||||
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
flatten bool
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
size: 1363315,
|
||||
flatten: true,
|
||||
objs: 14,
|
||||
oSizes: sizeHistogram{0: 6, 1: 8},
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
size: 21000,
|
||||
objs: 3,
|
||||
oSizes: sizeHistogram{0: 1, 1: 2},
|
||||
},
|
||||
{
|
||||
path: "/newfolder",
|
||||
size: 5,
|
||||
objs: 3,
|
||||
oSizes: sizeHistogram{0: 3},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira",
|
||||
size: 1300010,
|
||||
flatten: true,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "/nonexistying",
|
||||
isNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if w.flatten {
|
||||
*e = got.flatten(*e)
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
files = []usageTestFile{
|
||||
{
|
||||
name: "dir1/dira/dirasub/fileindira2",
|
||||
size: 200,
|
||||
},
|
||||
}
|
||||
|
||||
createUsageTestFiles(t, base, files)
|
||||
err = os.RemoveAll(filepath.Join(base, "dir1/dira/dirasub/dcfile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Changed dir must be picked up in this many cycles.
|
||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
flatten bool
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "/",
|
||||
size: 363515,
|
||||
flatten: true,
|
||||
objs: 14,
|
||||
oSizes: sizeHistogram{0: 7, 1: 7},
|
||||
},
|
||||
{
|
||||
path: "/dir1/dira",
|
||||
size: 300210,
|
||||
objs: 4,
|
||||
flatten: true,
|
||||
oSizes: sizeHistogram{0: 2, 1: 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if w.flatten {
|
||||
*e = got.flatten(*e)
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataUsageUpdatePrefix(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUpdateUsagePrefix")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
base = filepath.Join(base, "bucket")
|
||||
defer os.RemoveAll(base)
|
||||
var files = []usageTestFile{
|
||||
{name: "bucket/rootfile", size: 10000},
|
||||
{name: "bucket/rootfile2", size: 10000},
|
||||
{name: "bucket/dir1/d1file", size: 2000},
|
||||
{name: "bucket/dir2/d2file", size: 300},
|
||||
{name: "bucket/dir1/dira/dafile", size: 100000},
|
||||
{name: "bucket/dir1/dira/dbfile", size: 200000},
|
||||
{name: "bucket/dir1/dira/dirasub/dcfile", size: 1000000},
|
||||
{name: "bucket/dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||
}
|
||||
createUsageTestFiles(t, base, files)
|
||||
|
||||
getSize := func(item Item) (i int64, err error) {
|
||||
if item.Typ&os.ModeDir == 0 {
|
||||
s, err := os.Stat(item.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.Size(), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
got, err := updateUsage(context.Background(), base, dataUsageCache{Info: dataUsageCacheInfo{Name: "bucket"}}, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test dirs
|
||||
var want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "flat",
|
||||
size: 1322310,
|
||||
objs: 8,
|
||||
oSizes: sizeHistogram{0: 2, 1: 6},
|
||||
},
|
||||
{
|
||||
path: "bucket/",
|
||||
size: 20000,
|
||||
objs: 2,
|
||||
oSizes: sizeHistogram{1: 2},
|
||||
},
|
||||
{
|
||||
path: "bucket/dir1",
|
||||
size: 2000,
|
||||
objs: 1,
|
||||
oSizes: sizeHistogram{1: 1},
|
||||
},
|
||||
{
|
||||
path: "bucket/dir1/dira",
|
||||
size: 1300010,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "bucket/dir1/dira/",
|
||||
size: 1300010,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "bucket/nonexistying",
|
||||
isNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.path == "flat" {
|
||||
f := got.flatten(*got.root())
|
||||
e = &f
|
||||
}
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
files = []usageTestFile{
|
||||
{
|
||||
name: "bucket/newfolder/afile",
|
||||
size: 4,
|
||||
},
|
||||
{
|
||||
name: "bucket/newfolder/anotherone",
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
name: "bucket/newfolder/anemptyone",
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
name: "bucket/dir1/fileindir1",
|
||||
size: 20000,
|
||||
},
|
||||
{
|
||||
name: "bucket/dir1/dirc/fileindirc",
|
||||
size: 20000,
|
||||
},
|
||||
{
|
||||
name: "bucket/rootfile3",
|
||||
size: 1000,
|
||||
},
|
||||
}
|
||||
createUsageTestFiles(t, base, files)
|
||||
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "flat",
|
||||
size: 1363315,
|
||||
objs: 14,
|
||||
oSizes: sizeHistogram{0: 6, 1: 8},
|
||||
},
|
||||
{
|
||||
path: "bucket/",
|
||||
size: 21000,
|
||||
objs: 3,
|
||||
oSizes: sizeHistogram{0: 1, 1: 2},
|
||||
},
|
||||
{
|
||||
path: "bucket/newfolder",
|
||||
size: 5,
|
||||
objs: 3,
|
||||
oSizes: sizeHistogram{0: 3},
|
||||
},
|
||||
{
|
||||
path: "bucket/dir1/dira",
|
||||
size: 1300010,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 1, 1: 3},
|
||||
},
|
||||
{
|
||||
path: "bucket/nonexistying",
|
||||
isNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.path == "flat" {
|
||||
f := got.flatten(*got.root())
|
||||
e = &f
|
||||
}
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
files = []usageTestFile{
|
||||
{
|
||||
name: "bucket/dir1/dira/dirasub/fileindira2",
|
||||
size: 200,
|
||||
},
|
||||
}
|
||||
|
||||
createUsageTestFiles(t, base, files)
|
||||
err = os.RemoveAll(filepath.Join(base, "bucket/dir1/dira/dirasub/dcfile"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Changed dir must be picked up in this many cycles.
|
||||
for i := 0; i < dataUsageUpdateDirCycles; i++ {
|
||||
got, err = updateUsage(context.Background(), base, got, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
want = []struct {
|
||||
path string
|
||||
isNil bool
|
||||
size, objs int
|
||||
oSizes sizeHistogram
|
||||
}{
|
||||
{
|
||||
path: "flat",
|
||||
size: 363515,
|
||||
objs: 14,
|
||||
oSizes: sizeHistogram{0: 7, 1: 7},
|
||||
},
|
||||
{
|
||||
path: "bucket/dir1/dira",
|
||||
size: 300210,
|
||||
objs: 4,
|
||||
oSizes: sizeHistogram{0: 2, 1: 2},
|
||||
},
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
t.Run(w.path, func(t *testing.T) {
|
||||
e := got.find(w.path)
|
||||
if w.path == "flat" {
|
||||
f := got.flatten(*got.root())
|
||||
e = &f
|
||||
}
|
||||
if w.isNil {
|
||||
if e != nil {
|
||||
t.Error("want nil, got", e)
|
||||
}
|
||||
return
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatal("got nil result")
|
||||
}
|
||||
if e.Size != int64(w.size) {
|
||||
t.Error("got size", e.Size, "want", w.size)
|
||||
}
|
||||
if e.Objects != uint64(w.objs) {
|
||||
t.Error("got objects", e.Objects, "want", w.objs)
|
||||
}
|
||||
if e.ObjSizes != w.oSizes {
|
||||
t.Error("got histogram", e.ObjSizes, "want", w.oSizes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createUsageTestFiles(t *testing.T, base string, files []usageTestFile) {
|
||||
for _, f := range files {
|
||||
err := os.MkdirAll(filepath.Dir(filepath.Join(base, f.name)), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(base, f.name), make([]byte, f.size), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataUsageCacheSerialize(t *testing.T) {
|
||||
base, err := ioutil.TempDir("", "TestDataUsageCacheSerialize")
|
||||
if err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
defer os.RemoveAll(base)
|
||||
var files = []usageTestFile{
|
||||
{name: "rootfile", size: 10000},
|
||||
{name: "rootfile2", size: 10000},
|
||||
{name: "dir1/d1file", size: 2000},
|
||||
{name: "dir2/d2file", size: 300},
|
||||
{name: "dir1/dira/dafile", size: 100000},
|
||||
{name: "dir1/dira/dbfile", size: 200000},
|
||||
{name: "dir1/dira/dirasub/dcfile", size: 1000000},
|
||||
{name: "dir1/dira/dirasub/sublevel3/dccccfile", size: 10},
|
||||
}
|
||||
createUsageTestFiles(t, base, files)
|
||||
|
||||
getSize := func(item Item) (i int64, err error) {
|
||||
if item.Typ&os.ModeDir == 0 {
|
||||
s, err := os.Stat(item.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.Size(), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
want, err := updateUsage(context.Background(), base, dataUsageCache{}, func() {}, getSize)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b := want.serialize()
|
||||
var got dataUsageCache
|
||||
err = got.deserialize(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got.Info.LastUpdate.IsZero() {
|
||||
t.Error("lastupdate not set")
|
||||
}
|
||||
|
||||
if !want.Info.LastUpdate.Equal(got.Info.LastUpdate) {
|
||||
t.Fatalf("deserialize mismatch\nwant: %+v\ngot: %+v", want, got)
|
||||
}
|
||||
}
|
||||
+277
-180
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2019-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -37,7 +37,7 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/sio"
|
||||
"github.com/ncw/directio"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,7 +45,7 @@ const (
|
||||
cacheMetaJSONFile = "cache.json"
|
||||
cacheDataFile = "part.1"
|
||||
cacheMetaVersion = "1.0.0"
|
||||
|
||||
cacheExpiryDays = time.Duration(90 * time.Hour * 24) // defaults to 90 days
|
||||
// SSECacheEncrypted is the metadata key indicating that the object
|
||||
// is a cache entry encrypted with cache KMS master key in globalCacheKMS.
|
||||
SSECacheEncrypted = "X-Minio-Internal-Encrypted-Cache"
|
||||
@@ -68,6 +68,8 @@ type cacheMeta struct {
|
||||
Meta map[string]string `json:"meta,omitempty"`
|
||||
// Ranges maps cached range to associated filename.
|
||||
Ranges map[string]string `json:"ranges,omitempty"`
|
||||
// Hits is a counter on the number of times this object has been accessed so far.
|
||||
Hits int `json:"hits,omitempty"`
|
||||
}
|
||||
|
||||
// RangeInfo has the range, file and range length information for a cached range.
|
||||
@@ -125,44 +127,54 @@ func (m *cacheMeta) ToObjectInfo(bucket, object string) (o ObjectInfo) {
|
||||
type diskCache struct {
|
||||
dir string // caching directory
|
||||
quotaPct int // max usage in %
|
||||
expiry int // cache expiry in days
|
||||
// mark false if drive is offline
|
||||
online bool
|
||||
// mutex to protect updates to online variable
|
||||
onlineMutex *sync.RWMutex
|
||||
// purge() listens on this channel to start the cache-purge process
|
||||
purgeChan chan struct{}
|
||||
pool sync.Pool
|
||||
onlineMutex *sync.RWMutex
|
||||
pool sync.Pool
|
||||
after int // minimum accesses before an object is cached.
|
||||
lowWatermark int
|
||||
highWatermark int
|
||||
gcCounter atomic.Uint64
|
||||
// nsMutex namespace lock
|
||||
nsMutex *nsLockMap
|
||||
// Object functions pointing to the corresponding functions of backend implementation.
|
||||
NewNSLockFn func(ctx context.Context, cachePath string) RWLocker
|
||||
}
|
||||
|
||||
// Inits the disk cache dir if it is not initialized already.
|
||||
func newDiskCache(dir string, expiry int, quotaPct int) (*diskCache, error) {
|
||||
func newDiskCache(dir string, quotaPct, after, lowWatermark, highWatermark int) (*diskCache, error) {
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
return nil, fmt.Errorf("Unable to initialize '%s' dir, %w", dir, err)
|
||||
}
|
||||
cache := diskCache{
|
||||
dir: dir,
|
||||
expiry: expiry,
|
||||
quotaPct: quotaPct,
|
||||
purgeChan: make(chan struct{}),
|
||||
online: true,
|
||||
onlineMutex: &sync.RWMutex{},
|
||||
dir: dir,
|
||||
quotaPct: quotaPct,
|
||||
after: after,
|
||||
lowWatermark: lowWatermark,
|
||||
highWatermark: highWatermark,
|
||||
online: true,
|
||||
onlineMutex: &sync.RWMutex{},
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := directio.AlignedBlock(int(cacheBlkSize))
|
||||
b := disk.AlignedBlock(int(cacheBlkSize))
|
||||
return &b
|
||||
},
|
||||
},
|
||||
nsMutex: newNSLock(false),
|
||||
}
|
||||
cache.NewNSLockFn = func(ctx context.Context, cachePath string) RWLocker {
|
||||
return cache.nsMutex.NewNSLock(ctx, nil, cachePath, "")
|
||||
}
|
||||
return &cache, nil
|
||||
}
|
||||
|
||||
// Returns if the disk usage is low.
|
||||
// Disk usage is low if usage is < 80% of cacheMaxDiskUsagePct
|
||||
// Ex. for a 100GB disk, if maxUsage is configured as 70% then cacheMaxDiskUsagePct is 70G
|
||||
// hence disk usage is low if the disk usage is less than 56G (because 80% of 70G is 56G)
|
||||
// diskUsageLow() returns true if disk usage falls below the low watermark w.r.t configured cache quota.
|
||||
// Ex. for a 100GB disk, if quota is configured as 70% and watermark_low = 80% and
|
||||
// watermark_high = 90% then garbage collection starts when 63% of disk is used and
|
||||
// stops when disk usage drops to 56%
|
||||
func (c *diskCache) diskUsageLow() bool {
|
||||
minUsage := c.quotaPct * 80 / 100
|
||||
gcStopPct := c.quotaPct * c.lowWatermark / 100
|
||||
di, err := disk.GetInfo(c.dir)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||
@@ -171,21 +183,22 @@ func (c *diskCache) diskUsageLow() bool {
|
||||
return false
|
||||
}
|
||||
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
||||
return int(usedPercent) < minUsage
|
||||
return int(usedPercent) < gcStopPct
|
||||
}
|
||||
|
||||
// Return if the disk usage is high.
|
||||
// Disk usage is high if disk used is > cacheMaxDiskUsagePct
|
||||
// Returns if the disk usage reaches high water mark w.r.t the configured cache quota.
|
||||
// gc starts if high water mark reached.
|
||||
func (c *diskCache) diskUsageHigh() bool {
|
||||
gcTriggerPct := c.quotaPct * c.highWatermark / 100
|
||||
di, err := disk.GetInfo(c.dir)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
usedPercent := (di.Total - di.Free) * 100 / di.Total
|
||||
return int(usedPercent) > c.quotaPct
|
||||
return int(usedPercent) >= gcTriggerPct
|
||||
}
|
||||
|
||||
// Returns if size space can be allocated without exceeding
|
||||
@@ -202,12 +215,42 @@ func (c *diskCache) diskAvailable(size int64) bool {
|
||||
return int(usedPercent) < c.quotaPct
|
||||
}
|
||||
|
||||
// toClear returns how many bytes should be cleared to reach the low watermark quota.
|
||||
// returns 0 if below quota.
|
||||
func (c *diskCache) toClear() uint64 {
|
||||
di, err := disk.GetInfo(c.dir)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("cachePath", c.dir)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return 0
|
||||
}
|
||||
return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark))
|
||||
}
|
||||
|
||||
// Purge cache entries that were not accessed.
|
||||
func (c *diskCache) purge() {
|
||||
func (c *diskCache) purge(ctx context.Context) {
|
||||
if c.diskUsageLow() {
|
||||
return
|
||||
}
|
||||
toFree := c.toClear()
|
||||
if toFree == 0 {
|
||||
return
|
||||
}
|
||||
// expiry for cleaning up old cache.json files that
|
||||
// need to be cleaned up.
|
||||
expiry := UTCNow().Add(-cacheExpiryDays)
|
||||
// defaulting max hits count to 100
|
||||
scorer, err := newFileScorer(int64(toFree), time.Now().Unix(), 100)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
// this function returns FileInfo for cached range files and cache data file.
|
||||
fiStatFn := func(ranges map[string]string, dataFile, pathPrefix string) map[string]os.FileInfo {
|
||||
fm := make(map[string]os.FileInfo)
|
||||
fname := pathJoin(pathPrefix, cacheDataFile)
|
||||
fname := pathJoin(pathPrefix, dataFile)
|
||||
if fi, err := os.Stat(fname); err == nil {
|
||||
fm[fname] = fi
|
||||
}
|
||||
@@ -220,65 +263,73 @@ func (c *diskCache) purge() {
|
||||
}
|
||||
return fm
|
||||
}
|
||||
ctx := context.Background()
|
||||
for {
|
||||
olderThan := c.expiry * 24
|
||||
for !c.diskUsageLow() {
|
||||
// delete unaccessed objects older than expiry duration
|
||||
expiry := UTCNow().Add(time.Hour * time.Duration(-1*olderThan))
|
||||
olderThan /= 2
|
||||
if olderThan < 1 {
|
||||
break
|
||||
}
|
||||
deletedCount := 0
|
||||
objDirs, err := ioutil.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
objDirs, err := ioutil.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, obj := range objDirs {
|
||||
if obj.Name() == minioMetaBucket {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, obj := range objDirs {
|
||||
if obj.Name() == minioMetaBucket {
|
||||
continue
|
||||
}
|
||||
meta, _, err := c.statCachedMeta(pathJoin(c.dir, obj.Name()))
|
||||
if err != nil {
|
||||
// delete any partially filled cache entry left behind.
|
||||
removeAll(pathJoin(c.dir, obj.Name()))
|
||||
continue
|
||||
}
|
||||
// stat all cached file ranges and cacheDataFile.
|
||||
fis := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, obj.Name()))
|
||||
objInfo := meta.ToObjectInfo("", "")
|
||||
cc := cacheControlOpts(objInfo)
|
||||
|
||||
for fname, fi := range fis {
|
||||
if atime.Get(fi).Before(expiry) ||
|
||||
cc.isStale(objInfo.ModTime) {
|
||||
if err = removeAll(fname); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
deletedCount++
|
||||
// break early if sufficient disk space reclaimed.
|
||||
if !c.diskUsageLow() {
|
||||
break
|
||||
}
|
||||
cacheDir := pathJoin(c.dir, obj.Name())
|
||||
meta, _, numHits, err := c.statCachedMeta(ctx, cacheDir)
|
||||
if err != nil {
|
||||
// delete any partially filled cache entry left behind.
|
||||
removeAll(cacheDir)
|
||||
continue
|
||||
}
|
||||
// stat all cached file ranges and cacheDataFile.
|
||||
cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, obj.Name()))
|
||||
objInfo := meta.ToObjectInfo("", "")
|
||||
cc := cacheControlOpts(objInfo)
|
||||
for fname, fi := range cachedFiles {
|
||||
if cc != nil {
|
||||
if cc.isStale(objInfo.ModTime) {
|
||||
if err = removeAll(fname); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
scorer.adjustSaveBytes(-fi.Size())
|
||||
// break early if sufficient disk space reclaimed.
|
||||
if c.diskUsageLow() {
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if deletedCount == 0 {
|
||||
break
|
||||
}
|
||||
scorer.addFile(fname, atime.Get(fi), fi.Size(), numHits)
|
||||
}
|
||||
lastRunTime := time.Now()
|
||||
for {
|
||||
<-c.purgeChan
|
||||
timeElapsed := time.Since(lastRunTime)
|
||||
if timeElapsed > time.Hour {
|
||||
break
|
||||
}
|
||||
// clean up stale cache.json files for objects that never got cached but access count was maintained in cache.json
|
||||
fi, err := os.Stat(pathJoin(cacheDir, cacheMetaJSONFile))
|
||||
if err != nil || (fi.ModTime().Before(expiry) && len(cachedFiles) == 0) {
|
||||
removeAll(cacheDir)
|
||||
scorer.adjustSaveBytes(-fi.Size())
|
||||
continue
|
||||
}
|
||||
if c.diskUsageLow() {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, path := range scorer.fileNames() {
|
||||
removeAll(path)
|
||||
slashIdx := strings.LastIndex(path, SlashSeparator)
|
||||
pathPrefix := path[0:slashIdx]
|
||||
fname := path[slashIdx+1:]
|
||||
if fname == cacheDataFile {
|
||||
removeAll(pathPrefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *diskCache) incGCCounter() {
|
||||
c.gcCounter.Add(uint64(1))
|
||||
}
|
||||
func (c *diskCache) resetGCCounter() {
|
||||
c.gcCounter.Store(uint64(0))
|
||||
}
|
||||
func (c *diskCache) gcCount() uint64 {
|
||||
return c.gcCounter.Load()
|
||||
}
|
||||
|
||||
// sets cache drive status
|
||||
@@ -296,63 +347,67 @@ func (c *diskCache) IsOnline() bool {
|
||||
}
|
||||
|
||||
// Stat returns ObjectInfo from disk cache
|
||||
func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectInfo, err error) {
|
||||
func (c *diskCache) Stat(ctx context.Context, bucket, object string) (oi ObjectInfo, numHits int, err error) {
|
||||
var partial bool
|
||||
var meta *cacheMeta
|
||||
|
||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||
oi, err = c.statCache(cacheObjPath)
|
||||
// Stat the file to get file size.
|
||||
meta, partial, numHits, err = c.statCachedMeta(ctx, cacheObjPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if partial {
|
||||
return oi, numHits, errFileNotFound
|
||||
}
|
||||
oi = meta.ToObjectInfo("", "")
|
||||
oi.Bucket = bucket
|
||||
oi.Name = object
|
||||
|
||||
if err = decryptCacheObjectETag(&oi); err != nil {
|
||||
return oi, err
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// statCachedMeta returns metadata from cache - including ranges cached, partial to indicate
|
||||
// if partial object is cached.
|
||||
func (c *diskCache) statCachedMeta(cacheObjPath string) (meta *cacheMeta, partial bool, err error) {
|
||||
// Stat the file to get file size.
|
||||
metaPath := pathJoin(cacheObjPath, cacheMetaJSONFile)
|
||||
f, err := os.Open(metaPath)
|
||||
if err != nil {
|
||||
return meta, partial, err
|
||||
func (c *diskCache) statCachedMeta(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
|
||||
|
||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||
if err = cLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
meta = &cacheMeta{Version: cacheMetaVersion}
|
||||
if err := jsonLoad(f, meta); err != nil {
|
||||
return meta, partial, err
|
||||
}
|
||||
// get metadata of part.1 if full file has been cached.
|
||||
partial = true
|
||||
fi, err := os.Stat(pathJoin(cacheObjPath, cacheDataFile))
|
||||
if err == nil {
|
||||
meta.Stat.ModTime = atime.Get(fi)
|
||||
partial = false
|
||||
}
|
||||
return meta, partial, nil
|
||||
|
||||
defer cLock.RUnlock()
|
||||
return c.statCache(ctx, cacheObjPath)
|
||||
}
|
||||
|
||||
// statRange returns ObjectInfo and RangeInfo from disk cache
|
||||
func (c *diskCache) statRange(ctx context.Context, bucket, object string, rs *HTTPRangeSpec) (oi ObjectInfo, rngInfo RangeInfo, err error) {
|
||||
func (c *diskCache) statRange(ctx context.Context, bucket, object string, rs *HTTPRangeSpec) (oi ObjectInfo, rngInfo RangeInfo, numHits int, err error) {
|
||||
// Stat the file to get file size.
|
||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||
var meta *cacheMeta
|
||||
var partial bool
|
||||
|
||||
if rs == nil {
|
||||
oi, err = c.statCache(cacheObjPath)
|
||||
return oi, rngInfo, err
|
||||
}
|
||||
meta, _, err := c.statCachedMeta(cacheObjPath)
|
||||
meta, partial, numHits, err = c.statCachedMeta(ctx, cacheObjPath)
|
||||
if err != nil {
|
||||
return oi, rngInfo, err
|
||||
return
|
||||
}
|
||||
|
||||
oi = meta.ToObjectInfo("", "")
|
||||
oi.Bucket = bucket
|
||||
oi.Name = object
|
||||
if !partial {
|
||||
err = decryptCacheObjectETag(&oi)
|
||||
return
|
||||
}
|
||||
|
||||
actualSize := uint64(meta.Stat.Size)
|
||||
_, length, err := rs.GetOffsetLength(int64(actualSize))
|
||||
var length int64
|
||||
_, length, err = rs.GetOffsetLength(int64(actualSize))
|
||||
if err != nil {
|
||||
return oi, rngInfo, err
|
||||
return
|
||||
}
|
||||
|
||||
actualRngSize := uint64(length)
|
||||
@@ -363,38 +418,61 @@ func (c *diskCache) statRange(ctx context.Context, bucket, object string, rs *HT
|
||||
rng := rs.String(int64(actualSize))
|
||||
rngFile, ok := meta.Ranges[rng]
|
||||
if !ok {
|
||||
return oi, rngInfo, ObjectNotFound{Bucket: bucket, Object: object}
|
||||
return oi, rngInfo, numHits, ObjectNotFound{Bucket: bucket, Object: object}
|
||||
}
|
||||
if _, err = os.Stat(pathJoin(cacheObjPath, rngFile)); err != nil {
|
||||
return oi, rngInfo, numHits, ObjectNotFound{Bucket: bucket, Object: object}
|
||||
}
|
||||
rngInfo = RangeInfo{Range: rng, File: rngFile, Size: int64(actualRngSize)}
|
||||
|
||||
oi = meta.ToObjectInfo("", "")
|
||||
oi.Bucket = bucket
|
||||
oi.Name = object
|
||||
|
||||
if err = decryptCacheObjectETag(&oi); err != nil {
|
||||
return oi, rngInfo, err
|
||||
}
|
||||
err = decryptCacheObjectETag(&oi)
|
||||
return
|
||||
}
|
||||
|
||||
// statCache is a convenience function for purge() to get ObjectInfo for cached object
|
||||
func (c *diskCache) statCache(cacheObjPath string) (oi ObjectInfo, e error) {
|
||||
func (c *diskCache) statCache(ctx context.Context, cacheObjPath string) (meta *cacheMeta, partial bool, numHits int, err error) {
|
||||
// Stat the file to get file size.
|
||||
meta, partial, err := c.statCachedMeta(cacheObjPath)
|
||||
metaPath := pathJoin(cacheObjPath, cacheMetaJSONFile)
|
||||
f, err := os.Open(metaPath)
|
||||
if err != nil {
|
||||
return oi, err
|
||||
return meta, partial, 0, err
|
||||
}
|
||||
if partial {
|
||||
return oi, errFileNotFound
|
||||
defer f.Close()
|
||||
meta = &cacheMeta{Version: cacheMetaVersion}
|
||||
if err := jsonLoad(f, meta); err != nil {
|
||||
return meta, partial, 0, err
|
||||
}
|
||||
return meta.ToObjectInfo("", ""), nil
|
||||
// get metadata of part.1 if full file has been cached.
|
||||
partial = true
|
||||
fi, err := os.Stat(pathJoin(cacheObjPath, cacheDataFile))
|
||||
if err == nil {
|
||||
meta.Stat.ModTime = atime.Get(fi)
|
||||
partial = false
|
||||
}
|
||||
return meta, partial, meta.Hits, nil
|
||||
}
|
||||
|
||||
// saves object metadata to disk cache
|
||||
func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string) error {
|
||||
fileName := getCacheSHADir(c.dir, bucket, object)
|
||||
metaPath := pathJoin(fileName, cacheMetaJSONFile)
|
||||
// incHitsOnly is true if metadata update is incrementing only the hit counter
|
||||
func (c *diskCache) SaveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
|
||||
cachedPath := getCacheSHADir(c.dir, bucket, object)
|
||||
cLock := c.NewNSLockFn(ctx, cachedPath)
|
||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock()
|
||||
return c.saveMetadata(ctx, bucket, object, meta, actualSize, rs, rsFileName, incHitsOnly)
|
||||
}
|
||||
|
||||
// saves object metadata to disk cache
|
||||
// incHitsOnly is true if metadata update is incrementing only the hit counter
|
||||
func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, meta map[string]string, actualSize int64, rs *HTTPRangeSpec, rsFileName string, incHitsOnly bool) error {
|
||||
cachedPath := getCacheSHADir(c.dir, bucket, object)
|
||||
metaPath := pathJoin(cachedPath, cacheMetaJSONFile)
|
||||
// Create cache directory if needed
|
||||
if err := os.MkdirAll(cachedPath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(metaPath, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -405,6 +483,7 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
|
||||
if err := jsonLoad(f, m); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
// increment hits
|
||||
if rs != nil {
|
||||
if m.Ranges == nil {
|
||||
m.Ranges = make(map[string]string)
|
||||
@@ -413,46 +492,30 @@ func (c *diskCache) saveMetadata(ctx context.Context, bucket, object string, met
|
||||
} else {
|
||||
// this is necessary cleanup of range files if entire object is cached.
|
||||
for _, f := range m.Ranges {
|
||||
removeAll(pathJoin(fileName, f))
|
||||
removeAll(pathJoin(cachedPath, f))
|
||||
}
|
||||
m.Ranges = nil
|
||||
}
|
||||
m.Stat.Size = actualSize
|
||||
m.Stat.ModTime = UTCNow()
|
||||
m.Meta = meta
|
||||
if !incHitsOnly {
|
||||
// reset meta
|
||||
m.Meta = meta
|
||||
} else {
|
||||
if m.Meta == nil {
|
||||
m.Meta = make(map[string]string)
|
||||
}
|
||||
if etag, ok := meta["etag"]; ok {
|
||||
m.Meta["etag"] = etag
|
||||
}
|
||||
}
|
||||
|
||||
m.Hits++
|
||||
|
||||
m.Checksum = CacheChecksumInfoV1{Algorithm: HighwayHash256S.String(), Blocksize: cacheBlkSize}
|
||||
|
||||
return jsonSave(f, m)
|
||||
}
|
||||
|
||||
// Backend metadata could have changed through server side copy - reset cache metadata if that is the case
|
||||
func (c *diskCache) updateMetadataIfChanged(ctx context.Context, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo) error {
|
||||
|
||||
bkMeta := make(map[string]string)
|
||||
cacheMeta := make(map[string]string)
|
||||
for k, v := range bkObjectInfo.UserDefined {
|
||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
bkMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
for k, v := range cacheObjInfo.UserDefined {
|
||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
cacheMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
if !reflect.DeepEqual(bkMeta, cacheMeta) ||
|
||||
bkObjectInfo.ETag != cacheObjInfo.ETag ||
|
||||
bkObjectInfo.ContentType != cacheObjInfo.ContentType ||
|
||||
bkObjectInfo.Expires != cacheObjInfo.Expires {
|
||||
return c.saveMetadata(ctx, bucket, object, getMetadata(bkObjectInfo), bkObjectInfo.Size, nil, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCacheSHADir(dir, bucket, object string) string {
|
||||
return pathJoin(dir, getSHA256Hash([]byte(pathJoin(bucket, object))))
|
||||
}
|
||||
@@ -548,21 +611,40 @@ func newCacheEncryptMetadata(bucket, object string, metadata map[string]string)
|
||||
}
|
||||
|
||||
// Caches the object to disk
|
||||
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions) error {
|
||||
func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions, incHitsOnly bool) error {
|
||||
if c.diskUsageHigh() {
|
||||
select {
|
||||
case c.purgeChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
c.incGCCounter()
|
||||
io.Copy(ioutil.Discard, data)
|
||||
return errDiskFull
|
||||
}
|
||||
cachePath := getCacheSHADir(c.dir, bucket, object)
|
||||
cLock := c.NewNSLockFn(ctx, cachePath)
|
||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock()
|
||||
|
||||
meta, _, numHits, err := c.statCache(ctx, cachePath)
|
||||
// Case where object not yet cached
|
||||
if os.IsNotExist(err) && c.after >= 1 {
|
||||
return c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false)
|
||||
}
|
||||
// Case where object already has a cache metadata entry but not yet cached
|
||||
if err == nil && numHits < c.after {
|
||||
cETag := extractETag(meta.Meta)
|
||||
bETag := extractETag(opts.UserDefined)
|
||||
if cETag == bETag {
|
||||
return c.saveMetadata(ctx, bucket, object, opts.UserDefined, size, nil, "", false)
|
||||
}
|
||||
incHitsOnly = true
|
||||
}
|
||||
|
||||
if rs != nil {
|
||||
return c.putRange(ctx, bucket, object, data, size, rs, opts)
|
||||
}
|
||||
if !c.diskAvailable(size) {
|
||||
return errDiskFull
|
||||
}
|
||||
cachePath := getCacheSHADir(c.dir, bucket, object)
|
||||
if err := os.MkdirAll(cachePath, 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -572,7 +654,6 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
||||
}
|
||||
var reader = data
|
||||
var actualSize = uint64(size)
|
||||
var err error
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||
if err != nil {
|
||||
@@ -588,11 +669,12 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
||||
removeAll(cachePath)
|
||||
return err
|
||||
}
|
||||
|
||||
if actualSize != uint64(n) {
|
||||
removeAll(cachePath)
|
||||
return IncompleteBody{}
|
||||
}
|
||||
return c.saveMetadata(ctx, bucket, object, metadata, n, nil, "")
|
||||
return c.saveMetadata(ctx, bucket, object, metadata, n, nil, "", incHitsOnly)
|
||||
}
|
||||
|
||||
// Caches the range to disk
|
||||
@@ -638,7 +720,7 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
|
||||
removeAll(cachePath)
|
||||
return IncompleteBody{}
|
||||
}
|
||||
return c.saveMetadata(ctx, bucket, object, metadata, int64(objSize), rs, cacheFile)
|
||||
return c.saveMetadata(ctx, bucket, object, metadata, int64(objSize), rs, cacheFile, false)
|
||||
}
|
||||
|
||||
// checks streaming bitrot checksum of cached object before returning data
|
||||
@@ -738,13 +820,18 @@ func (c *diskCache) bitrotReadFromCache(ctx context.Context, filePath string, of
|
||||
}
|
||||
|
||||
// Get returns ObjectInfo and reader for object from disk cache
|
||||
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
var objInfo ObjectInfo
|
||||
func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, numHits int, err error) {
|
||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||
var rngInfo RangeInfo
|
||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||
if err := cLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
return nil, numHits, err
|
||||
}
|
||||
|
||||
if objInfo, rngInfo, err = c.statRange(ctx, bucket, object, rs); err != nil {
|
||||
return nil, toObjectErr(err, bucket, object)
|
||||
defer cLock.RUnlock()
|
||||
var objInfo ObjectInfo
|
||||
var rngInfo RangeInfo
|
||||
if objInfo, rngInfo, numHits, err = c.statRange(ctx, bucket, object, rs); err != nil {
|
||||
return nil, numHits, toObjectErr(err, bucket, object)
|
||||
}
|
||||
cacheFile := cacheDataFile
|
||||
objSize := objInfo.Size
|
||||
@@ -760,12 +847,13 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
if HasSuffix(object, SlashSeparator) {
|
||||
// The lock taken above is released when
|
||||
// objReader.Close() is called by the caller.
|
||||
return NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts.CheckCopyPrecondFn, nsUnlocker)
|
||||
gr, gerr := NewGetObjectReaderFromReader(bytes.NewBuffer(nil), objInfo, opts.CheckCopyPrecondFn, nsUnlocker)
|
||||
return gr, numHits, gerr
|
||||
}
|
||||
|
||||
fn, off, length, nErr := NewGetObjectReader(rs, objInfo, opts.CheckCopyPrecondFn, nsUnlocker)
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
return nil, numHits, nErr
|
||||
}
|
||||
filePath := pathJoin(cacheObjPath, cacheFile)
|
||||
pr, pw := io.Pipe()
|
||||
@@ -782,7 +870,7 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
|
||||
gr, gerr := fn(pr, h, opts.CheckCopyPrecondFn, pipeCloser)
|
||||
if gerr != nil {
|
||||
return gr, gerr
|
||||
return gr, numHits, gerr
|
||||
}
|
||||
if globalCacheKMS != nil {
|
||||
// clean up internal SSE cache metadata
|
||||
@@ -792,15 +880,24 @@ func (c *diskCache) Get(ctx context.Context, bucket, object string, rs *HTTPRang
|
||||
// overlay Size with actual object size and not the range size
|
||||
gr.ObjInfo.Size = objSize
|
||||
}
|
||||
return gr, nil
|
||||
return gr, numHits, nil
|
||||
|
||||
}
|
||||
|
||||
// Deletes the cached object
|
||||
func (c *diskCache) Delete(ctx context.Context, bucket, object string) (err error) {
|
||||
cachePath := getCacheSHADir(c.dir, bucket, object)
|
||||
return removeAll(cachePath)
|
||||
func (c *diskCache) delete(ctx context.Context, cacheObjPath string) (err error) {
|
||||
cLock := c.NewNSLockFn(ctx, cacheObjPath)
|
||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock()
|
||||
return removeAll(cacheObjPath)
|
||||
}
|
||||
|
||||
// Deletes the cached object
|
||||
func (c *diskCache) Delete(ctx context.Context, bucket, object string) (err error) {
|
||||
cacheObjPath := getCacheSHADir(c.dir, bucket, object)
|
||||
return c.delete(ctx, cacheObjPath)
|
||||
}
|
||||
|
||||
// convenience function to check if object is cached on this diskCache
|
||||
|
||||
+187
-11
@@ -17,8 +17,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -57,13 +61,8 @@ type cacheControl struct {
|
||||
noCache bool
|
||||
}
|
||||
|
||||
func (c cacheControl) isEmpty() bool {
|
||||
return c == cacheControl{}
|
||||
|
||||
}
|
||||
|
||||
func (c cacheControl) isStale(modTime time.Time) bool {
|
||||
if c.isEmpty() {
|
||||
func (c *cacheControl) isStale(modTime time.Time) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
// response will never be stale if only-if-cached is set
|
||||
@@ -100,7 +99,8 @@ func (c cacheControl) isStale(modTime time.Time) bool {
|
||||
}
|
||||
|
||||
// returns struct with cache-control settings from user metadata.
|
||||
func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
||||
func cacheControlOpts(o ObjectInfo) *cacheControl {
|
||||
c := cacheControl{}
|
||||
m := o.UserDefined
|
||||
if o.Expires != timeSentinel {
|
||||
c.expiry = o.Expires
|
||||
@@ -114,7 +114,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
||||
|
||||
}
|
||||
if headerVal == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
headerVal = strings.ToLower(headerVal)
|
||||
headerVal = strings.TrimSpace(headerVal)
|
||||
@@ -146,7 +146,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
||||
p[0] == "max-stale" {
|
||||
i, err := strconv.Atoi(p[1])
|
||||
if err != nil {
|
||||
return cacheControl{}
|
||||
return nil
|
||||
}
|
||||
if p[0] == "max-age" {
|
||||
c.maxAge = i
|
||||
@@ -162,7 +162,7 @@ func cacheControlOpts(o ObjectInfo) (c cacheControl) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return c
|
||||
return &c
|
||||
}
|
||||
|
||||
// backendDownError returns true if err is due to backend failure or faulty disk if in server mode
|
||||
@@ -265,3 +265,179 @@ func decryptCacheObjectETag(info *ObjectInfo) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isMetadataSame(m1, m2 map[string]string) bool {
|
||||
if m1 == nil && m2 == nil {
|
||||
return true
|
||||
}
|
||||
if (m1 == nil && m2 != nil) || (m2 == nil && m1 != nil) {
|
||||
return false
|
||||
}
|
||||
if len(m1) != len(m2) {
|
||||
return false
|
||||
}
|
||||
for k1, v1 := range m1 {
|
||||
if v2, ok := m2[k1]; !ok || (v1 != v2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type fileScorer struct {
|
||||
saveBytes int64
|
||||
now int64
|
||||
maxHits int
|
||||
// 1/size for consistent score.
|
||||
sizeMult float64
|
||||
|
||||
// queue is a linked list of files we want to delete.
|
||||
// The list is kept sorted according to score, highest at top, lowest at bottom.
|
||||
queue list.List
|
||||
queuedBytes int64
|
||||
}
|
||||
|
||||
type queuedFile struct {
|
||||
name string
|
||||
size int64
|
||||
score float64
|
||||
}
|
||||
|
||||
// newFileScorer allows to collect files to save a specific number of bytes.
|
||||
// Each file is assigned a score based on its age, size and number of hits.
|
||||
// A list of files is maintained
|
||||
func newFileScorer(saveBytes int64, now int64, maxHits int) (*fileScorer, error) {
|
||||
if saveBytes <= 0 {
|
||||
return nil, errors.New("newFileScorer: saveBytes <= 0")
|
||||
}
|
||||
if now < 0 {
|
||||
return nil, errors.New("newFileScorer: now < 0")
|
||||
}
|
||||
if maxHits <= 0 {
|
||||
return nil, errors.New("newFileScorer: maxHits <= 0")
|
||||
}
|
||||
f := fileScorer{saveBytes: saveBytes, maxHits: maxHits, now: now, sizeMult: 1 / float64(saveBytes)}
|
||||
f.queue.Init()
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (f *fileScorer) addFile(name string, lastAccess time.Time, size int64, hits int) {
|
||||
// Calculate how much we want to delete this object.
|
||||
file := queuedFile{
|
||||
name: name,
|
||||
size: size,
|
||||
}
|
||||
score := float64(f.now - lastAccess.Unix())
|
||||
// Size as fraction of how much we want to save, 0->1.
|
||||
szWeight := math.Max(0, (math.Min(1, float64(size)*f.sizeMult)))
|
||||
// 0 at f.maxHits, 1 at 0.
|
||||
hitsWeight := (1.0 - math.Max(0, math.Min(1.0, float64(hits)/float64(f.maxHits))))
|
||||
file.score = score * (1 + 0.25*szWeight + 0.25*hitsWeight)
|
||||
// If we still haven't saved enough, just add the file
|
||||
if f.queuedBytes < f.saveBytes {
|
||||
f.insertFile(file)
|
||||
f.trimQueue()
|
||||
return
|
||||
}
|
||||
// If we score less than the worst, don't insert.
|
||||
worstE := f.queue.Back()
|
||||
if worstE != nil && file.score < worstE.Value.(queuedFile).score {
|
||||
return
|
||||
}
|
||||
f.insertFile(file)
|
||||
f.trimQueue()
|
||||
}
|
||||
|
||||
// adjustSaveBytes allows to adjust the number of bytes to save.
|
||||
// This can be used to adjust the count on the fly.
|
||||
// Returns true if there still is a need to delete files (saveBytes >0),
|
||||
// false if no more bytes needs to be saved.
|
||||
func (f *fileScorer) adjustSaveBytes(n int64) bool {
|
||||
f.saveBytes += n
|
||||
if f.saveBytes <= 0 {
|
||||
f.queue.Init()
|
||||
f.saveBytes = 0
|
||||
return false
|
||||
}
|
||||
if n < 0 {
|
||||
f.trimQueue()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// insertFile will insert a file into the list, sorted by its score.
|
||||
func (f *fileScorer) insertFile(file queuedFile) {
|
||||
e := f.queue.Front()
|
||||
for e != nil {
|
||||
v := e.Value.(queuedFile)
|
||||
if v.score < file.score {
|
||||
break
|
||||
}
|
||||
e = e.Next()
|
||||
}
|
||||
f.queuedBytes += file.size
|
||||
// We reached the end.
|
||||
if e == nil {
|
||||
f.queue.PushBack(file)
|
||||
return
|
||||
}
|
||||
f.queue.InsertBefore(file, e)
|
||||
}
|
||||
|
||||
// trimQueue will trim the back of queue and still keep below wantSave.
|
||||
func (f *fileScorer) trimQueue() {
|
||||
for {
|
||||
e := f.queue.Back()
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
v := e.Value.(queuedFile)
|
||||
if f.queuedBytes-v.size < f.saveBytes {
|
||||
return
|
||||
}
|
||||
f.queue.Remove(e)
|
||||
f.queuedBytes -= v.size
|
||||
}
|
||||
}
|
||||
|
||||
// fileNames returns all queued file names.
|
||||
func (f *fileScorer) fileNames() []string {
|
||||
res := make([]string, 0, f.queue.Len())
|
||||
e := f.queue.Front()
|
||||
for e != nil {
|
||||
res = append(res, e.Value.(queuedFile).name)
|
||||
e = e.Next()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (f *fileScorer) reset() {
|
||||
f.queue.Init()
|
||||
f.queuedBytes = 0
|
||||
}
|
||||
|
||||
func (f *fileScorer) queueString() string {
|
||||
var res strings.Builder
|
||||
e := f.queue.Front()
|
||||
i := 0
|
||||
for e != nil {
|
||||
v := e.Value.(queuedFile)
|
||||
if i > 0 {
|
||||
res.WriteByte('\n')
|
||||
}
|
||||
res.WriteString(fmt.Sprintf("%03d: %s (score: %.3f, bytes: %d)", i, v.name, v.score, v.size))
|
||||
i++
|
||||
e = e.Next()
|
||||
}
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// bytesToClear() returns the number of bytes to clear to reach low watermark
|
||||
// w.r.t quota given disk total and free space, quota in % allocated to cache
|
||||
// and low watermark % w.r.t allowed quota.
|
||||
func bytesToClear(total, free int64, quotaPct, lowWatermark uint64) uint64 {
|
||||
used := (total - free)
|
||||
quotaAllowed := total * (int64)(quotaPct) / 100
|
||||
lowWMUsage := (total * (int64)(lowWatermark*quotaPct) / (100 * 100))
|
||||
return (uint64)(math.Min(float64(quotaAllowed), math.Max(0.0, float64(used-lowWMUsage))))
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ func TestGetCacheControlOpts(t *testing.T) {
|
||||
testCases := []struct {
|
||||
cacheControlHeaderVal string
|
||||
expiryHeaderVal time.Time
|
||||
expectedCacheControl cacheControl
|
||||
expectedCacheControl *cacheControl
|
||||
expectedErr bool
|
||||
}{
|
||||
{"", timeSentinel, cacheControl{}, false},
|
||||
{"max-age=2592000, public", timeSentinel, cacheControl{maxAge: 2592000, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"max-age=2592000, no-store", timeSentinel, cacheControl{maxAge: 2592000, sMaxAge: 0, noStore: true, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"must-revalidate, max-age=600", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"s-maxAge=2500, max-age=600", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"s-maxAge=2500, max-age=600", expiry, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 07, 28, 00, 00, time.UTC)}, false},
|
||||
{"s-maxAge=2500, max-age=600s", timeSentinel, cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, true},
|
||||
{"", timeSentinel, nil, false},
|
||||
{"max-age=2592000, public", timeSentinel, &cacheControl{maxAge: 2592000, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"max-age=2592000, no-store", timeSentinel, &cacheControl{maxAge: 2592000, sMaxAge: 0, noStore: true, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"must-revalidate, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 0, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"s-maxAge=2500, max-age=600", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, false},
|
||||
{"s-maxAge=2500, max-age=600", expiry, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Date(2015, time.October, 21, 07, 28, 00, 00, time.UTC)}, false},
|
||||
{"s-maxAge=2500, max-age=600s", timeSentinel, &cacheControl{maxAge: 600, sMaxAge: 2500, minFresh: 0, expiry: time.Time{}}, true},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
@@ -49,7 +49,7 @@ func TestGetCacheControlOpts(t *testing.T) {
|
||||
m["expires"] = testCase.expiryHeaderVal.String()
|
||||
}
|
||||
c := cacheControlOpts(ObjectInfo{UserDefined: m, Expires: testCase.expiryHeaderVal})
|
||||
if testCase.expectedErr && (c != cacheControl{}) {
|
||||
if testCase.expectedErr && (c != nil) {
|
||||
t.Errorf("expected err, got <nil>")
|
||||
}
|
||||
if !testCase.expectedErr && !reflect.DeepEqual(c, testCase.expectedCacheControl) {
|
||||
@@ -58,3 +58,115 @@ func TestGetCacheControlOpts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMetadataSame(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
m1 map[string]string
|
||||
m2 map[string]string
|
||||
expected bool
|
||||
}{
|
||||
{nil, nil, true},
|
||||
{nil, map[string]string{}, false},
|
||||
{map[string]string{"k": "v"}, map[string]string{"k": "v"}, true},
|
||||
{map[string]string{"k": "v"}, map[string]string{"a": "b"}, false},
|
||||
{map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "v1", "k2": "v1"}, false},
|
||||
{map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "v1", "k2": "v2"}, true},
|
||||
{map[string]string{"K1": "v1", "k2": "v2"}, map[string]string{"k1": "v1", "k2": "v2"}, false},
|
||||
{map[string]string{"k1": "v1", "k2": "v2", "k3": "v3"}, map[string]string{"k1": "v1", "k2": "v2"}, false},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actual := isMetadataSame(testCase.m1, testCase.m2)
|
||||
if testCase.expected != actual {
|
||||
t.Errorf("test %d expected %v, got %v", i, testCase.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileScorer(t *testing.T) {
|
||||
fs, err := newFileScorer(1000, time.Now().Unix(), 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fs.fileNames()) != 0 {
|
||||
t.Fatal("non zero files??")
|
||||
}
|
||||
now := time.Now()
|
||||
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||
fs.addFile("older", now.Add(-time.Hour), 1000, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"older"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
fs.reset()
|
||||
fs.addFile("bigger", now.Add(-time.Minute), 2000, 10)
|
||||
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"bigger"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
fs.reset()
|
||||
fs.addFile("less", now.Add(-time.Minute), 1000, 5)
|
||||
fs.addFile("recent", now.Add(-time.Minute), 1000, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"less"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
fs.reset()
|
||||
fs.addFile("small", now.Add(-time.Minute), 200, 10)
|
||||
fs.addFile("medium", now.Add(-time.Minute), 300, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"medium", "small"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
fs.addFile("large", now.Add(-time.Minute), 700, 10)
|
||||
fs.addFile("xsmol", now.Add(-time.Minute), 7, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"large", "medium"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
|
||||
fs.reset()
|
||||
fs.addFile("less", now.Add(-time.Minute), 500, 5)
|
||||
fs.addFile("recent", now.Add(-time.Minute), 500, 10)
|
||||
if !fs.adjustSaveBytes(-500) {
|
||||
t.Fatal("we should still need more bytes, got false")
|
||||
}
|
||||
// We should only need 500 bytes now.
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"less"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
if fs.adjustSaveBytes(-500) {
|
||||
t.Fatal("we shouldn't need any more bytes, got true")
|
||||
}
|
||||
fs, err = newFileScorer(1000, time.Now().Unix(), 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fs.addFile("bigger", now.Add(-time.Minute), 50, 10)
|
||||
// sorting should be consistent after adjusting savebytes.
|
||||
fs.adjustSaveBytes(-800)
|
||||
fs.addFile("smaller", now.Add(-time.Minute), 40, 10)
|
||||
if !reflect.DeepEqual(fs.fileNames(), []string{"bigger", "smaller"}) {
|
||||
t.Fatal("unexpected file list", fs.queueString())
|
||||
}
|
||||
}
|
||||
func TestBytesToClear(t *testing.T) {
|
||||
testCases := []struct {
|
||||
total int64
|
||||
free int64
|
||||
quotaPct uint64
|
||||
watermarkLow uint64
|
||||
expected uint64
|
||||
}{
|
||||
{1000, 800, 40, 90, 0},
|
||||
{1000, 200, 40, 90, 400},
|
||||
{1000, 400, 40, 90, 240},
|
||||
{1000, 600, 40, 90, 40},
|
||||
{1000, 600, 40, 70, 120},
|
||||
{1000, 1000, 90, 70, 0},
|
||||
{1000, 0, 90, 70, 370},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
toClear := bytesToClear(tc.total, tc.free, tc.quotaPct, tc.watermarkLow)
|
||||
if tc.expected != toClear {
|
||||
t.Errorf("test %d expected %v, got %v", i, tc.expected, toClear)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+137
-91
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2019,2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,13 +31,15 @@ import (
|
||||
"github.com/djherbis/atime"
|
||||
"github.com/minio/minio/cmd/config/cache"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||
"github.com/minio/minio/pkg/color"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/minio/minio/pkg/wildcard"
|
||||
)
|
||||
|
||||
const (
|
||||
cacheBlkSize = int64(1 * 1024 * 1024)
|
||||
cacheBlkSize = int64(1 * 1024 * 1024)
|
||||
cacheGCInterval = time.Minute * 30
|
||||
)
|
||||
|
||||
// CacheStorageInfo - represents total, free capacity of
|
||||
@@ -55,6 +57,7 @@ type CacheObjectLayer interface {
|
||||
DeleteObject(ctx context.Context, bucket, object string) error
|
||||
DeleteObjects(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
PutObject(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
CopyObject(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
// Storage operations.
|
||||
StorageInfo(ctx context.Context) CacheStorageInfo
|
||||
CacheStats() *CacheStats
|
||||
@@ -66,74 +69,57 @@ type cacheObjects struct {
|
||||
cache []*diskCache
|
||||
// file path patterns to exclude from cache
|
||||
exclude []string
|
||||
|
||||
// number of accesses after which to cache an object
|
||||
after int
|
||||
// if true migration is in progress from v1 to v2
|
||||
migrating bool
|
||||
// mutex to protect migration bool
|
||||
migMutex sync.Mutex
|
||||
|
||||
// nsMutex namespace lock
|
||||
nsMutex *nsLockMap
|
||||
|
||||
// Cache stats
|
||||
cacheStats *CacheStats
|
||||
|
||||
// Object functions pointing to the corresponding functions of backend implementation.
|
||||
NewNSLockFn func(ctx context.Context, bucket, object string) RWLocker
|
||||
GetObjectNInfoFn func(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error)
|
||||
GetObjectInfoFn func(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
DeleteObjectFn func(ctx context.Context, bucket, object string) error
|
||||
DeleteObjectsFn func(ctx context.Context, bucket string, objects []string) ([]error, error)
|
||||
PutObjectFn func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
CopyObjectFn func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error)
|
||||
}
|
||||
|
||||
func (c *cacheObjects) delete(ctx context.Context, dcache *diskCache, bucket, object string) (err error) {
|
||||
cLock := c.NewNSLockFn(ctx, bucket, object)
|
||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock()
|
||||
return dcache.Delete(ctx, bucket, object)
|
||||
func (c *cacheObjects) incHitsToMeta(ctx context.Context, dcache *diskCache, bucket, object string, size int64, eTag string) error {
|
||||
metadata := make(map[string]string)
|
||||
metadata["etag"] = eTag
|
||||
return dcache.SaveMetadata(ctx, bucket, object, metadata, size, nil, "", true)
|
||||
}
|
||||
|
||||
func (c *cacheObjects) put(ctx context.Context, dcache *diskCache, bucket, object string, data io.Reader, size int64, rs *HTTPRangeSpec, opts ObjectOptions) error {
|
||||
cLock := c.NewNSLockFn(ctx, bucket, object)
|
||||
if err := cLock.GetLock(globalObjectTimeout); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cLock.Unlock()
|
||||
return dcache.Put(ctx, bucket, object, data, size, rs, opts)
|
||||
}
|
||||
// Backend metadata could have changed through server side copy - reset cache metadata if that is the case
|
||||
func (c *cacheObjects) updateMetadataIfChanged(ctx context.Context, dcache *diskCache, bucket, object string, bkObjectInfo, cacheObjInfo ObjectInfo) error {
|
||||
|
||||
func (c *cacheObjects) get(ctx context.Context, dcache *diskCache, bucket, object string, rs *HTTPRangeSpec, h http.Header, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
cLock := c.NewNSLockFn(ctx, bucket, object)
|
||||
if err := cLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
return nil, err
|
||||
bkMeta := make(map[string]string)
|
||||
cacheMeta := make(map[string]string)
|
||||
for k, v := range bkObjectInfo.UserDefined {
|
||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
bkMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
for k, v := range cacheObjInfo.UserDefined {
|
||||
if HasPrefix(k, ReservedMetadataPrefix) {
|
||||
// Do not need to send any internal metadata
|
||||
continue
|
||||
}
|
||||
cacheMeta[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
defer cLock.RUnlock()
|
||||
return dcache.Get(ctx, bucket, object, rs, h, opts)
|
||||
}
|
||||
|
||||
func (c *cacheObjects) stat(ctx context.Context, dcache *diskCache, bucket, object string) (oi ObjectInfo, err error) {
|
||||
cLock := c.NewNSLockFn(ctx, bucket, object)
|
||||
if err := cLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
return oi, err
|
||||
if !isMetadataSame(bkMeta, cacheMeta) ||
|
||||
bkObjectInfo.ETag != cacheObjInfo.ETag ||
|
||||
bkObjectInfo.ContentType != cacheObjInfo.ContentType ||
|
||||
!bkObjectInfo.Expires.Equal(cacheObjInfo.Expires) {
|
||||
return dcache.SaveMetadata(ctx, bucket, object, getMetadata(bkObjectInfo), bkObjectInfo.Size, nil, "", false)
|
||||
}
|
||||
|
||||
defer cLock.RUnlock()
|
||||
return dcache.Stat(ctx, bucket, object)
|
||||
}
|
||||
|
||||
func (c *cacheObjects) statRange(ctx context.Context, dcache *diskCache, bucket, object string, rs *HTTPRangeSpec) (oi ObjectInfo, err error) {
|
||||
cLock := c.NewNSLockFn(ctx, bucket, object)
|
||||
if err := cLock.GetRLock(globalObjectTimeout); err != nil {
|
||||
return oi, err
|
||||
}
|
||||
|
||||
defer cLock.RUnlock()
|
||||
oi, _, err = dcache.statRange(ctx, bucket, object, rs)
|
||||
return oi, err
|
||||
return c.incHitsToMeta(ctx, dcache, bucket, object, cacheObjInfo.Size, cacheObjInfo.ETag)
|
||||
}
|
||||
|
||||
// DeleteObject clears cache entry if backend delete operation succeeds
|
||||
@@ -149,9 +135,7 @@ func (c *cacheObjects) DeleteObject(ctx context.Context, bucket, object string)
|
||||
if cerr != nil {
|
||||
return
|
||||
}
|
||||
if dcache.Exists(ctx, bucket, object) {
|
||||
c.delete(ctx, dcache, bucket, object)
|
||||
}
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,7 +175,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
if c.isCacheExclude(bucket, object) || c.skipCache() {
|
||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
var cc cacheControl
|
||||
var cc *cacheControl
|
||||
var cacheObjSize int64
|
||||
// fetch diskCache if object is currently cached or nearest available cache drive
|
||||
dcache, err := c.getCacheToLoc(ctx, bucket, object)
|
||||
@@ -199,7 +183,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
|
||||
cacheReader, cacheErr := c.get(ctx, dcache, bucket, object, rs, h, opts)
|
||||
cacheReader, numCacheHits, cacheErr := dcache.Get(ctx, bucket, object, rs, h, opts)
|
||||
if cacheErr == nil {
|
||||
cacheObjSize = cacheReader.ObjInfo.Size
|
||||
if rs != nil {
|
||||
@@ -208,8 +192,8 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
}
|
||||
}
|
||||
cc = cacheControlOpts(cacheReader.ObjInfo)
|
||||
if (!cc.isEmpty() && !cc.isStale(cacheReader.ObjInfo.ModTime)) ||
|
||||
cc.onlyIfCached {
|
||||
if cc != nil && (!cc.isStale(cacheReader.ObjInfo.ModTime) ||
|
||||
cc.onlyIfCached) {
|
||||
// This is a cache hit, mark it so
|
||||
bytesServed := cacheReader.ObjInfo.Size
|
||||
if rs != nil {
|
||||
@@ -219,9 +203,10 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
}
|
||||
c.cacheStats.incHit()
|
||||
c.cacheStats.incBytesServed(bytesServed)
|
||||
c.incHitsToMeta(ctx, dcache, bucket, object, cacheReader.ObjInfo.Size, cacheReader.ObjInfo.ETag)
|
||||
return cacheReader, nil
|
||||
}
|
||||
if cc.noStore {
|
||||
if cc != nil && cc.noStore {
|
||||
c.cacheStats.incMiss()
|
||||
bReader, err := c.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
bReader.ObjInfo.CacheLookupStatus = CacheHit
|
||||
@@ -251,32 +236,46 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
c.cacheStats.incMiss()
|
||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
|
||||
// skip cache for objects with locks
|
||||
objRetention := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||
legalHold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
|
||||
c.cacheStats.incMiss()
|
||||
return c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
if cacheErr == nil {
|
||||
// if ETag matches for stale cache entry, serve from cache
|
||||
if cacheReader.ObjInfo.ETag == objInfo.ETag {
|
||||
// Update metadata in case server-side copy might have changed object metadata
|
||||
dcache.updateMetadataIfChanged(ctx, bucket, object, objInfo, cacheReader.ObjInfo)
|
||||
c.updateMetadataIfChanged(ctx, dcache, bucket, object, objInfo, cacheReader.ObjInfo)
|
||||
c.incCacheStats(cacheObjSize)
|
||||
return cacheReader, nil
|
||||
}
|
||||
cacheReader.Close()
|
||||
// Object is stale, so delete from cache
|
||||
c.delete(ctx, dcache, bucket, object)
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
}
|
||||
|
||||
// Reaching here implies cache miss
|
||||
c.cacheStats.incMiss()
|
||||
// Since we got here, we are serving the request from backend,
|
||||
// and also adding the object to the cache.
|
||||
if !dcache.diskUsageLow() {
|
||||
select {
|
||||
case dcache.purgeChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
if dcache.diskUsageHigh() {
|
||||
dcache.incGCCounter()
|
||||
}
|
||||
|
||||
bkReader, bkErr := c.GetObjectNInfoFn(ctx, bucket, object, rs, h, lockType, opts)
|
||||
|
||||
if bkErr != nil {
|
||||
return bkReader, bkErr
|
||||
}
|
||||
// If object has less hits than configured cache after, just increment the hit counter
|
||||
// but do not cache it.
|
||||
if numCacheHits < c.after {
|
||||
c.incHitsToMeta(ctx, dcache, bucket, object, objInfo.Size, objInfo.ETag)
|
||||
return bkReader, bkErr
|
||||
}
|
||||
|
||||
// Record if cache has a hit that was invalidated by ETag verification
|
||||
if cacheErr == nil {
|
||||
bkReader.ObjInfo.CacheLookupStatus = CacheHit
|
||||
@@ -293,10 +292,10 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
return
|
||||
}
|
||||
defer bReader.Close()
|
||||
oi, err := c.statRange(ctx, dcache, bucket, object, rs)
|
||||
oi, _, _, err := dcache.statRange(ctx, bucket, object, rs)
|
||||
// avoid cache overwrite if another background routine filled cache
|
||||
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
||||
c.put(ctx, dcache, bucket, object, bReader, bReader.ObjInfo.Size, rs, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)})
|
||||
dcache.Put(ctx, bucket, object, bReader, bReader.ObjInfo.Size, rs, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
|
||||
}
|
||||
}()
|
||||
return bkReader, bkErr
|
||||
@@ -306,7 +305,7 @@ func (c *cacheObjects) GetObjectNInfo(ctx context.Context, bucket, object string
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
teeReader := io.TeeReader(bkReader, pipeWriter)
|
||||
go func() {
|
||||
putErr := c.put(ctx, dcache, bucket, object, io.LimitReader(pipeReader, bkReader.ObjInfo.Size), bkReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bkReader.ObjInfo)})
|
||||
putErr := dcache.Put(ctx, bucket, object, io.LimitReader(pipeReader, bkReader.ObjInfo.Size), bkReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bkReader.ObjInfo)}, false)
|
||||
// close the write end of the pipe, so the error gets
|
||||
// propagated to getObjReader
|
||||
pipeWriter.CloseWithError(putErr)
|
||||
@@ -329,12 +328,12 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
||||
if err != nil {
|
||||
return getObjectInfoFn(ctx, bucket, object, opts)
|
||||
}
|
||||
var cc cacheControl
|
||||
var cc *cacheControl
|
||||
// if cache control setting is valid, avoid HEAD operation to backend
|
||||
cachedObjInfo, cerr := c.stat(ctx, dcache, bucket, object)
|
||||
cachedObjInfo, _, cerr := dcache.Stat(ctx, bucket, object)
|
||||
if cerr == nil {
|
||||
cc = cacheControlOpts(cachedObjInfo)
|
||||
if !cc.isStale(cachedObjInfo.ModTime) {
|
||||
if cc == nil || (cc != nil && !cc.isStale(cachedObjInfo.ModTime)) {
|
||||
// This is a cache hit, mark it so
|
||||
c.cacheStats.incHit()
|
||||
return cachedObjInfo, nil
|
||||
@@ -345,7 +344,7 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
||||
if err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
// Delete the cached entry if backend object was deleted.
|
||||
c.delete(ctx, dcache, bucket, object)
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
c.cacheStats.incMiss()
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
@@ -369,11 +368,35 @@ func (c *cacheObjects) GetObjectInfo(ctx context.Context, bucket, object string,
|
||||
}
|
||||
if cachedObjInfo.ETag != objInfo.ETag {
|
||||
// Delete the cached entry if the backend object was replaced.
|
||||
c.delete(ctx, dcache, bucket, object)
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
}
|
||||
return objInfo, nil
|
||||
}
|
||||
|
||||
// CopyObject reverts to backend after evicting any stale cache entries
|
||||
func (c *cacheObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
copyObjectFn := c.CopyObjectFn
|
||||
if c.isCacheExclude(srcBucket, srcObject) || c.skipCache() {
|
||||
return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
if srcBucket != dstBucket || srcObject != dstObject {
|
||||
return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
// fetch diskCache if object is currently cached or nearest available cache drive
|
||||
dcache, err := c.getCacheToLoc(ctx, srcBucket, srcObject)
|
||||
if err != nil {
|
||||
return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
// if currently cached, evict old entry and revert to backend.
|
||||
if cachedObjInfo, _, cerr := dcache.Stat(ctx, srcBucket, srcObject); cerr == nil {
|
||||
cc := cacheControlOpts(cachedObjInfo)
|
||||
if cc == nil || !cc.isStale(cachedObjInfo.ModTime) {
|
||||
dcache.Delete(ctx, srcBucket, srcObject)
|
||||
}
|
||||
}
|
||||
return copyObjectFn(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo, srcOpts, dstOpts)
|
||||
}
|
||||
|
||||
// StorageInfo - returns underlying storage statistics.
|
||||
func (c *cacheObjects) StorageInfo(ctx context.Context) (cInfo CacheStorageInfo) {
|
||||
var total, free uint64
|
||||
@@ -497,16 +520,10 @@ func newCache(config cache.Config) ([]*diskCache, bool, error) {
|
||||
if quota == 0 {
|
||||
quota = config.Quota
|
||||
}
|
||||
|
||||
cache, err := newDiskCache(dir, config.Expiry, quota)
|
||||
cache, err := newDiskCache(dir, quota, config.After, config.WatermarkLow, config.WatermarkHigh)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
// Start the purging go-routine for entries that have expired if no migration in progress
|
||||
if !migrating {
|
||||
go cache.purge()
|
||||
}
|
||||
|
||||
caches = append(caches, cache)
|
||||
}
|
||||
return caches, migrating, nil
|
||||
@@ -553,13 +570,12 @@ func (c *cacheObjects) migrateCacheFromV1toV2(ctx context.Context) {
|
||||
}
|
||||
|
||||
errCnt := 0
|
||||
for index, err := range g.Wait() {
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
errCnt++
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
go c.cache[index].purge()
|
||||
}
|
||||
|
||||
if errCnt > 0 {
|
||||
@@ -596,8 +612,9 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
||||
}
|
||||
|
||||
// skip cache for objects with locks
|
||||
objRetention := getObjectRetentionMeta(opts.UserDefined)
|
||||
if objRetention.Mode == Governance || objRetention.Mode == Compliance {
|
||||
objRetention := objectlock.GetObjectRetentionMeta(opts.UserDefined)
|
||||
legalHold := objectlock.GetObjectLegalHoldMeta(opts.UserDefined)
|
||||
if objRetention.Mode != objectlock.Invalid || legalHold.Status != "" {
|
||||
dcache.Delete(ctx, bucket, object)
|
||||
return putObjectFn(ctx, bucket, object, r, opts)
|
||||
}
|
||||
@@ -619,10 +636,10 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
||||
return
|
||||
}
|
||||
defer bReader.Close()
|
||||
oi, err := c.stat(ctx, dcache, bucket, object)
|
||||
oi, _, err := dcache.Stat(ctx, bucket, object)
|
||||
// avoid cache overwrite if another background routine filled cache
|
||||
if err != nil || oi.ETag != bReader.ObjInfo.ETag {
|
||||
c.put(ctx, dcache, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)})
|
||||
dcache.Put(ctx, bucket, object, bReader, bReader.ObjInfo.Size, nil, ObjectOptions{UserDefined: getMetadata(bReader.ObjInfo)}, false)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -638,13 +655,12 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &cacheObjects{
|
||||
cache: cache,
|
||||
exclude: config.Exclude,
|
||||
after: config.After,
|
||||
migrating: migrateSw,
|
||||
migMutex: sync.Mutex{},
|
||||
nsMutex: newNSLock(false),
|
||||
cacheStats: newCacheStats(),
|
||||
GetObjectInfoFn: func(ctx context.Context, bucket, object string, opts ObjectOptions) (ObjectInfo, error) {
|
||||
return newObjectLayerFn().GetObjectInfo(ctx, bucket, object, opts)
|
||||
@@ -665,13 +681,43 @@ func newServerCacheObjects(ctx context.Context, config cache.Config) (CacheObjec
|
||||
PutObjectFn: func(ctx context.Context, bucket, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
return newObjectLayerFn().PutObject(ctx, bucket, object, data, opts)
|
||||
},
|
||||
}
|
||||
c.NewNSLockFn = func(ctx context.Context, bucket, object string) RWLocker {
|
||||
return c.nsMutex.NewNSLock(ctx, nil, bucket, object)
|
||||
CopyObjectFn: func(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
return newObjectLayerFn().CopyObject(ctx, srcBucket, srcObject, destBucket, destObject, srcInfo, srcOpts, dstOpts)
|
||||
},
|
||||
}
|
||||
|
||||
if migrateSw {
|
||||
go c.migrateCacheFromV1toV2(ctx)
|
||||
}
|
||||
go c.gc(ctx)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *cacheObjects) gc(ctx context.Context) {
|
||||
ticker := time.NewTicker(cacheGCInterval)
|
||||
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.migrating {
|
||||
continue
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for _, dcache := range c.cache {
|
||||
if dcache.gcCount() == 0 {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(d *diskCache) {
|
||||
defer wg.Done()
|
||||
d.resetGCCounter()
|
||||
d.purge(ctx)
|
||||
}(dcache)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-89
@@ -20,22 +20,21 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
)
|
||||
|
||||
// Initialize cache objects.
|
||||
func initCacheObjects(disk string, cacheMaxUse int) (*diskCache, error) {
|
||||
return newDiskCache(disk, 80, cacheMaxUse)
|
||||
func initCacheObjects(disk string, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh int) (*diskCache, error) {
|
||||
return newDiskCache(disk, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh)
|
||||
}
|
||||
|
||||
// inits diskCache struct for nDisks
|
||||
func initDiskCaches(drives []string, cacheMaxUse int, t *testing.T) ([]*diskCache, error) {
|
||||
func initDiskCaches(drives []string, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh int, t *testing.T) ([]*diskCache, error) {
|
||||
var cb []*diskCache
|
||||
for _, d := range drives {
|
||||
obj, err := initCacheObjects(d, cacheMaxUse)
|
||||
obj, err := initCacheObjects(d, cacheMaxUse, cacheAfter, cacheWatermarkLow, cacheWatermarkHigh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,7 +69,7 @@ func TestGetCachedLoc(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, 100, t)
|
||||
d, err := initDiskCaches(fsDirs, 100, 1, 80, 90, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -109,7 +108,7 @@ func TestGetCacheMaxUse(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, 80, t)
|
||||
d, err := initDiskCaches(fsDirs, 80, 1, 80, 90, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -174,91 +173,13 @@ func TestCacheExclusion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Test diskCache.
|
||||
func TestDiskCache(t *testing.T) {
|
||||
fsDirs, err := getRandomDisks(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, 100, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := cacheObjects{cache: d}
|
||||
|
||||
cache := c.cache[0]
|
||||
ctx := context.Background()
|
||||
bucketName := "testbucket"
|
||||
objectName := "testobject"
|
||||
content := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
etag := "061208c10af71a30c6dcd6cf5d89f0fe"
|
||||
contentType := "application/zip"
|
||||
size := len(content)
|
||||
|
||||
httpMeta := make(map[string]string)
|
||||
httpMeta["etag"] = etag
|
||||
httpMeta["content-type"] = contentType
|
||||
|
||||
objInfo := ObjectInfo{}
|
||||
objInfo.Bucket = bucketName
|
||||
objInfo.Name = objectName
|
||||
objInfo.Size = int64(size)
|
||||
objInfo.ContentType = contentType
|
||||
objInfo.ETag = etag
|
||||
objInfo.UserDefined = httpMeta
|
||||
var opts ObjectOptions
|
||||
byteReader := bytes.NewReader([]byte(content))
|
||||
hashReader, err := hash.NewReader(byteReader, int64(size), "", "", int64(size), globalCLIContext.StrictS3Compat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cReader, err := cache.Get(ctx, bucketName, objectName, nil, http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
}, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cachedObjInfo := cReader.ObjInfo
|
||||
if !cache.Exists(ctx, bucketName, objectName) {
|
||||
t.Fatal("Expected object to exist on cache")
|
||||
}
|
||||
if cachedObjInfo.ETag != objInfo.ETag {
|
||||
t.Fatal("Expected ETag to match")
|
||||
}
|
||||
if cachedObjInfo.Size != objInfo.Size {
|
||||
t.Fatal("Size mismatch")
|
||||
}
|
||||
if cachedObjInfo.ContentType != objInfo.ContentType {
|
||||
t.Fatal("Cached content-type does not match")
|
||||
}
|
||||
writer := bytes.NewBuffer(nil)
|
||||
_, err = io.Copy(writer, cReader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ccontent := writer.Bytes(); !bytes.Equal([]byte(content), ccontent) {
|
||||
t.Errorf("wrong cached file content")
|
||||
}
|
||||
cReader.Close()
|
||||
|
||||
cache.Delete(ctx, bucketName, objectName)
|
||||
online := cache.IsOnline()
|
||||
if !online {
|
||||
t.Errorf("expected cache drive to be online")
|
||||
}
|
||||
}
|
||||
|
||||
// Test diskCache with upper bound on max cache use.
|
||||
func TestDiskCacheMaxUse(t *testing.T) {
|
||||
fsDirs, err := getRandomDisks(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := initDiskCaches(fsDirs, 80, t)
|
||||
d, err := initDiskCaches(fsDirs, 90, 0, 90, 99, t)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -290,16 +211,16 @@ func TestDiskCacheMaxUse(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cache.diskAvailable(int64(size)) {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta})
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta}, false)
|
||||
if err != errDiskFull {
|
||||
t.Fatal("Cache max-use limit violated.")
|
||||
}
|
||||
} else {
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta})
|
||||
err = cache.Put(ctx, bucketName, objectName, hashReader, hashReader.Size(), nil, ObjectOptions{UserDefined: httpMeta}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cReader, err := cache.Get(ctx, bucketName, objectName, nil, nil, opts)
|
||||
cReader, _, err := cache.Get(ctx, bucketName, objectName, nil, nil, opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+4
-56
@@ -21,27 +21,14 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
// Data types used for returning dummy tagging XML.
|
||||
// These variables shouldn't be used elsewhere.
|
||||
// They are only defined to be used in this file alone.
|
||||
|
||||
type tagging struct {
|
||||
XMLName xml.Name `xml:"Tagging"`
|
||||
TagSet tagSet `xml:"TagSet"`
|
||||
}
|
||||
|
||||
type tagSet struct {
|
||||
Tag []tagElem `xml:"Tag"`
|
||||
}
|
||||
|
||||
type tagElem struct {
|
||||
Key string `xml:"Key"`
|
||||
Value string `xml:"Value"`
|
||||
}
|
||||
|
||||
// GetBucketWebsite - GET bucket website, a dummy api
|
||||
func (api objectAPIHandlers) GetBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -171,47 +158,8 @@ func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
tags := &tagging{}
|
||||
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
|
||||
|
||||
if err := xml.NewEncoder(w).Encode(tags); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// GetObjectTaggingHandler - GET object tagging, a dummy api
|
||||
func (api objectAPIHandlers) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetObjectTagging")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object := vars["object"]
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Allow getObjectTagging if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate if object exists, before proceeding further...
|
||||
_, err := objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
tags := &tagging{}
|
||||
tags.TagSet.Tag = append(tags.TagSet.Tag, tagElem{})
|
||||
tags := &tagging.Tagging{}
|
||||
tags.TagSet.Tags = append(tags.TagSet.Tags, tagging.Tag{})
|
||||
|
||||
if err := xml.NewEncoder(w).Encode(tags); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
|
||||
@@ -280,15 +280,11 @@ func TestGetDecryptedRange_Issue50(t *testing.T) {
|
||||
Parts: []ObjectPartInfo{
|
||||
{
|
||||
Number: 1,
|
||||
Name: "part.1",
|
||||
ETag: "etag1",
|
||||
Size: 297580380,
|
||||
ActualSize: 297435132,
|
||||
},
|
||||
{
|
||||
Number: 2,
|
||||
Name: "part.2",
|
||||
ETag: "etag2",
|
||||
Size: 297580380,
|
||||
ActualSize: 297435132,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2018-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,7 +40,7 @@ type endpointSet struct {
|
||||
|
||||
// Supported set sizes this is used to find the optimal
|
||||
// single set size.
|
||||
var setSizes = []uint64{4, 6, 8, 10, 12, 14, 16}
|
||||
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
|
||||
// getDivisibleSize - returns a greatest common divisor of
|
||||
// all the ellipses sizes.
|
||||
@@ -60,7 +60,7 @@ func getDivisibleSize(totalSizes []uint64) (result uint64) {
|
||||
|
||||
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
||||
var isValidSetSize = func(count uint64) bool {
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1] && count%2 == 0)
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
|
||||
}
|
||||
|
||||
// getSetIndexes returns list of indexes which provides the set size
|
||||
@@ -108,12 +108,24 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
return ss
|
||||
}
|
||||
|
||||
setCounts := possibleSetCounts(commonSize)
|
||||
if len(setCounts) == 0 {
|
||||
msg := fmt.Sprintf("Incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
|
||||
return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
|
||||
}
|
||||
|
||||
if customSetDriveCount > 0 {
|
||||
msg := fmt.Sprintf("Invalid set drive count, leads to non-uniform distribution for the given number of disks. Possible values for custom set count are %d", possibleSetCounts(setSize))
|
||||
msg := fmt.Sprintf("Invalid set drive count. Acceptable values for %d number drives are %d", commonSize, setCounts)
|
||||
if customSetDriveCount > setSize {
|
||||
return nil, config.ErrInvalidErasureSetSize(nil).Msg(msg)
|
||||
}
|
||||
if setSize%customSetDriveCount != 0 {
|
||||
var found bool
|
||||
for _, ss := range setCounts {
|
||||
if ss == customSetDriveCount {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, config.ErrInvalidErasureSetSize(nil).Msg(msg)
|
||||
}
|
||||
setSize = customSetDriveCount
|
||||
@@ -121,7 +133,7 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
|
||||
// Check whether setSize is with the supported range.
|
||||
if !isValidSetSize(setSize) {
|
||||
msg := fmt.Sprintf("Incorrect number of endpoints provided %s", args)
|
||||
msg := fmt.Sprintf("Incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
|
||||
return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
|
||||
}
|
||||
|
||||
@@ -261,9 +273,6 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
if err != nil {
|
||||
return nil, -1, -1, config.ErrInvalidErasureSetSize(err)
|
||||
}
|
||||
if !isValidSetSize(uint64(setDriveCount)) {
|
||||
return nil, -1, -1, config.ErrInvalidErasureSetSize(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if !ellipses.HasEllipses(args...) {
|
||||
|
||||
@@ -104,6 +104,36 @@ func TestGetSetIndexesEnvOverride(t *testing.T) {
|
||||
8,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...12}/data{1...12}"},
|
||||
[]uint64{144},
|
||||
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16}},
|
||||
16,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{0...5}/data{1...28}"},
|
||||
[]uint64{168},
|
||||
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
|
||||
12,
|
||||
true,
|
||||
},
|
||||
// Incorrect custom set drive count.
|
||||
{
|
||||
[]string{"http://host{0...5}/data{1...28}"},
|
||||
[]uint64{168},
|
||||
nil,
|
||||
10,
|
||||
false,
|
||||
},
|
||||
// Failure not divisible number of disks.
|
||||
{
|
||||
[]string{"http://host{1...11}/data{1...11}"},
|
||||
[]uint64{121},
|
||||
[][]uint64{{11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11}},
|
||||
11,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...60}"},
|
||||
nil,
|
||||
@@ -153,12 +183,6 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
success bool
|
||||
}{
|
||||
// Invalid inputs.
|
||||
{
|
||||
[]string{"data{1...27}"},
|
||||
[]uint64{27},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...3}"},
|
||||
[]uint64{3},
|
||||
@@ -172,6 +196,12 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
// Valid inputs.
|
||||
{
|
||||
[]string{"data{1...27}"},
|
||||
[]uint64{27},
|
||||
[][]uint64{{9, 9, 9}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
|
||||
[]uint64{4, 8, 12},
|
||||
@@ -193,7 +223,7 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
{
|
||||
[]string{"data/controller{1...11}/export{1...8}"},
|
||||
[]uint64{88},
|
||||
[][]uint64{{8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}},
|
||||
[][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -262,12 +292,6 @@ func TestParseEndpointSet(t *testing.T) {
|
||||
endpointSet{},
|
||||
false,
|
||||
},
|
||||
// Indivisible range.
|
||||
{
|
||||
"{1...27}",
|
||||
endpointSet{},
|
||||
false,
|
||||
},
|
||||
// No range specified.
|
||||
{
|
||||
"{...}",
|
||||
@@ -293,6 +317,23 @@ func TestParseEndpointSet(t *testing.T) {
|
||||
false,
|
||||
},
|
||||
// Tests valid inputs.
|
||||
{
|
||||
"{1...27}",
|
||||
endpointSet{
|
||||
[]ellipses.ArgPattern{
|
||||
[]ellipses.Pattern{
|
||||
{
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
Seq: getSequences(1, 27, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[][]uint64{{9, 9, 9}},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"/export/set{1...64}",
|
||||
endpointSet{
|
||||
|
||||
+21
-3
@@ -171,7 +171,11 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||
if isHostIP(arg) {
|
||||
return ep, fmt.Errorf("invalid URL endpoint format: missing scheme http or https")
|
||||
}
|
||||
u = &url.URL{Path: path.Clean(arg)}
|
||||
absArg, err := filepath.Abs(arg)
|
||||
if err != nil {
|
||||
return Endpoint{}, fmt.Errorf("absolute path failed %s", err)
|
||||
}
|
||||
u = &url.URL{Path: path.Clean(absArg)}
|
||||
isLocal = true
|
||||
}
|
||||
|
||||
@@ -220,14 +224,28 @@ func (l EndpointZones) HTTPS() bool {
|
||||
return l[0].Endpoints.HTTPS()
|
||||
}
|
||||
|
||||
// Nodes - returns all nodes count
|
||||
func (l EndpointZones) Nodes() (count int) {
|
||||
// NEndpoints - returns all nodes count
|
||||
func (l EndpointZones) NEndpoints() (count int) {
|
||||
for _, ep := range l {
|
||||
count += len(ep.Endpoints)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Hosts - returns list of unique hosts
|
||||
func (l EndpointZones) Hosts() []string {
|
||||
foundSet := set.NewStringSet()
|
||||
for _, ep := range l {
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if foundSet.Contains(endpoint.Host) {
|
||||
continue
|
||||
}
|
||||
foundSet.Add(endpoint.Host)
|
||||
}
|
||||
}
|
||||
return foundSet.ToSlice()
|
||||
}
|
||||
|
||||
// Endpoints - list of same type of endpoint.
|
||||
type Endpoints []Endpoint
|
||||
|
||||
|
||||
+4
-20
@@ -37,19 +37,7 @@ func TestNewEndpoint(t *testing.T) {
|
||||
expectedType EndpointType
|
||||
expectedErr error
|
||||
}{
|
||||
{"foo", Endpoint{URL: &url.URL{Path: "foo"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"/foo", Endpoint{URL: &url.URL{Path: "/foo"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{`\foo`, Endpoint{URL: &url.URL{Path: `\foo`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"C", Endpoint{URL: &url.URL{Path: `C`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"C:", Endpoint{URL: &url.URL{Path: `C:`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"C:/", Endpoint{URL: &url.URL{Path: "C:"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{`C:\`, Endpoint{URL: &url.URL{Path: `C:\`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{`C:\foo`, Endpoint{URL: &url.URL{Path: `C:\foo`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"C:/foo", Endpoint{URL: &url.URL{Path: "C:/foo"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{`C:\\foo`, Endpoint{URL: &url.URL{Path: `C:\\foo`}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http:path", Endpoint{URL: &url.URL{Path: "http:path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http:/path", Endpoint{URL: &url.URL{Path: "http:/path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"http:///path", Endpoint{URL: &url.URL{Path: "http:/path"}, IsLocal: true}, PathEndpointType, nil},
|
||||
{"https://example.org/path", Endpoint{URL: u2, IsLocal: false}, URLEndpointType, nil},
|
||||
{"http://192.168.253.200/path", Endpoint{URL: u4, IsLocal: false}, URLEndpointType, nil},
|
||||
{"", Endpoint{}, -1, fmt.Errorf("empty or root endpoint is not supported")},
|
||||
@@ -101,7 +89,6 @@ func TestNewEndpoints(t *testing.T) {
|
||||
args []string
|
||||
expectedErr error
|
||||
}{
|
||||
{[]string{"d1", "d2", "d3", "d4"}, nil},
|
||||
{[]string{"/d1", "/d2", "/d3", "/d4"}, nil},
|
||||
{[]string{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}, nil},
|
||||
{[]string{"http://example.org/d1", "http://example.com/d1", "http://example.net/d1", "http://example.edu/d1"}, nil},
|
||||
@@ -225,20 +212,17 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
|
||||
// FS Setup
|
||||
{"localhost:9000", [][]string{{"http://localhost/d1"}}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
|
||||
{":443", [][]string{{"d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: "d1"}, IsLocal: true}}, FSSetupType, nil},
|
||||
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: "/d1"}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: "/d1"}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:10000", [][]string{{"./d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: "d1"}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:10000", [][]string{{`\d1`}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: `\d1`}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:10000", [][]string{{`.\d1`}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: `.\d1`}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:9000", [][]string{{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
|
||||
|
||||
// XL Setup with PathEndpointType
|
||||
{":1234", [][]string{{"/d1", "/d2", "d3", "d4"}}, ":1234",
|
||||
{":1234", [][]string{{"/d1", "/d2", "/d3", "/d4"}}, ":1234",
|
||||
Endpoints{
|
||||
Endpoint{URL: &url.URL{Path: "/d1"}, IsLocal: true},
|
||||
Endpoint{URL: &url.URL{Path: "/d2"}, IsLocal: true},
|
||||
Endpoint{URL: &url.URL{Path: "d3"}, IsLocal: true},
|
||||
Endpoint{URL: &url.URL{Path: "d4"}, IsLocal: true},
|
||||
Endpoint{URL: &url.URL{Path: "/d3"}, IsLocal: true},
|
||||
Endpoint{URL: &url.URL{Path: "/d4"}, IsLocal: true},
|
||||
}, XLSetupType, nil},
|
||||
// DistXL Setup with URLEndpointType
|
||||
{":9000", [][]string{{"http://localhost/d1", "http://localhost/d2", "http://localhost/d3", "http://localhost/d4"}}, ":9000", Endpoints{
|
||||
|
||||
+47
-8
@@ -18,12 +18,16 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
var errHealRequired = errors.New("heal required")
|
||||
|
||||
// Reads in parallel from readers.
|
||||
type parallelReader struct {
|
||||
readers []io.ReaderAt
|
||||
@@ -72,6 +76,7 @@ func (p *parallelReader) Read() ([][]byte, error) {
|
||||
readTriggerCh <- true
|
||||
}
|
||||
|
||||
healRequired := int32(0) // Atomic bool flag.
|
||||
readerIndex := 0
|
||||
var wg sync.WaitGroup
|
||||
// if readTrigger is true, it implies next disk.ReadAt() should be tried
|
||||
@@ -109,6 +114,9 @@ func (p *parallelReader) Read() ([][]byte, error) {
|
||||
p.buf[i] = p.buf[i][:p.shardSize]
|
||||
_, err := disk.ReadAt(p.buf[i], p.offset)
|
||||
if err != nil {
|
||||
if _, ok := err.(*errHashMismatch); ok {
|
||||
atomic.StoreInt32(&healRequired, 1)
|
||||
}
|
||||
p.readers[i] = nil
|
||||
// Since ReadAt returned error, trigger another read.
|
||||
readTriggerCh <- true
|
||||
@@ -126,24 +134,49 @@ func (p *parallelReader) Read() ([][]byte, error) {
|
||||
|
||||
if p.canDecode(newBuf) {
|
||||
p.offset += p.shardSize
|
||||
if healRequired != 0 {
|
||||
return newBuf, errHealRequired
|
||||
}
|
||||
return newBuf, nil
|
||||
}
|
||||
|
||||
return nil, errXLReadQuorum
|
||||
}
|
||||
|
||||
type errDecodeHealRequired struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (err *errDecodeHealRequired) Error() string {
|
||||
return err.err.Error()
|
||||
}
|
||||
|
||||
func (err *errDecodeHealRequired) Unwrap() error {
|
||||
return err.err
|
||||
}
|
||||
|
||||
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||
func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64) error {
|
||||
healRequired, err := e.decode(ctx, writer, readers, offset, length, totalLength)
|
||||
if healRequired {
|
||||
return &errDecodeHealRequired{err}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Decode reads from readers, reconstructs data if needed and writes the data to the writer.
|
||||
func (e Erasure) decode(ctx context.Context, writer io.Writer, readers []io.ReaderAt, offset, length, totalLength int64) (bool, error) {
|
||||
if offset < 0 || length < 0 {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return errInvalidArgument
|
||||
return false, errInvalidArgument
|
||||
}
|
||||
if offset+length > totalLength {
|
||||
logger.LogIf(ctx, errInvalidArgument)
|
||||
return errInvalidArgument
|
||||
return false, errInvalidArgument
|
||||
}
|
||||
if length == 0 {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
reader := newParallelReader(readers, e, offset, totalLength)
|
||||
@@ -151,6 +184,7 @@ func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.Read
|
||||
startBlock := offset / e.blockSize
|
||||
endBlock := (offset + length) / e.blockSize
|
||||
|
||||
var healRequired bool
|
||||
var bytesWritten int64
|
||||
for block := startBlock; block <= endBlock; block++ {
|
||||
var blockOffset, blockLength int64
|
||||
@@ -173,21 +207,26 @@ func (e Erasure) Decode(ctx context.Context, writer io.Writer, readers []io.Read
|
||||
}
|
||||
bufs, err := reader.Read()
|
||||
if err != nil {
|
||||
return err
|
||||
if errors.Is(err, errHealRequired) {
|
||||
healRequired = true
|
||||
} else {
|
||||
return healRequired, err
|
||||
}
|
||||
}
|
||||
if err = e.DecodeDataBlocks(bufs); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
return healRequired, err
|
||||
}
|
||||
n, err := writeDataBlocks(ctx, writer, bufs, e.dataBlocks, blockOffset, blockLength)
|
||||
if err != nil {
|
||||
return err
|
||||
return healRequired, err
|
||||
}
|
||||
bytesWritten += n
|
||||
}
|
||||
if bytesWritten != length {
|
||||
logger.LogIf(ctx, errLessData)
|
||||
return errLessData
|
||||
return healRequired, errLessData
|
||||
}
|
||||
return nil
|
||||
|
||||
return healRequired, nil
|
||||
}
|
||||
|
||||
+3
-194
@@ -11,216 +11,25 @@ package cmd
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrTraverseLink is used as a return value from WalkFuncs to indicate that the
|
||||
// symlink named in the call may be traversed.
|
||||
var ErrTraverseLink = errors.New("fastwalk: traverse symlink, assuming target is a directory")
|
||||
var errSkipFile = errors.New("fastwalk: skip this file")
|
||||
|
||||
// ErrSkipFiles is a used as a return value from WalkFuncs to indicate that the
|
||||
// callback should not be called for any other files in the current directory.
|
||||
// Child directories will still be traversed.
|
||||
var ErrSkipFiles = errors.New("fastwalk: skip remaining files in directory")
|
||||
|
||||
// Walk is a faster implementation of filepath.Walk.
|
||||
//
|
||||
// filepath.Walk's design necessarily calls os.Lstat on each file,
|
||||
// even if the caller needs less info.
|
||||
// Many tools need only the type of each file.
|
||||
// On some platforms, this information is provided directly by the readdir
|
||||
// system call, avoiding the need to stat each file individually.
|
||||
// fastwalk_unix.go contains a fork of the syscall routines.
|
||||
//
|
||||
// See golang.org/issue/16399
|
||||
//
|
||||
// Walk walks the file tree rooted at root, calling walkFn for
|
||||
// each file or directory in the tree, including root.
|
||||
//
|
||||
// If fastWalk returns filepath.SkipDir, the directory is skipped.
|
||||
//
|
||||
// Unlike filepath.Walk:
|
||||
// * file stat calls must be done by the user.
|
||||
// The only provided metadata is the file type, which does not include
|
||||
// any permission bits.
|
||||
// * multiple goroutines stat the filesystem concurrently. The provided
|
||||
// walkFn must be safe for concurrent use.
|
||||
// * fastWalk can follow symlinks if walkFn returns the TraverseLink
|
||||
// sentinel error. It is the walkFn's responsibility to prevent
|
||||
// fastWalk from going into symlink cycles.
|
||||
func fastWalk(root string, walkFn func(path string, typ os.FileMode) error) error {
|
||||
// TODO(bradfitz): make numWorkers configurable? We used a
|
||||
// minimum of 4 to give the kernel more info about multiple
|
||||
// things we want, in hopes its I/O scheduling can take
|
||||
// advantage of that. Hopefully most are in cache. Maybe 4 is
|
||||
// even too low of a minimum. Profile more.
|
||||
numWorkers := 4
|
||||
if n := runtime.NumCPU(); n > numWorkers {
|
||||
numWorkers = n
|
||||
}
|
||||
|
||||
// Make sure to wait for all workers to finish, otherwise
|
||||
// walkFn could still be called after returning. This Wait call
|
||||
// runs after close(e.donec) below.
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
w := &walker{
|
||||
fn: walkFn,
|
||||
enqueuec: make(chan walkItem, numWorkers), // buffered for performance
|
||||
workc: make(chan walkItem, numWorkers), // buffered for performance
|
||||
donec: make(chan struct{}),
|
||||
|
||||
// buffered for correctness & not leaking goroutines:
|
||||
resc: make(chan error, numWorkers),
|
||||
}
|
||||
defer close(w.donec)
|
||||
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
wg.Add(1)
|
||||
go w.doWork(&wg)
|
||||
}
|
||||
todo := []walkItem{{dir: root}}
|
||||
out := 0
|
||||
for {
|
||||
workc := w.workc
|
||||
var workItem walkItem
|
||||
if len(todo) == 0 {
|
||||
workc = nil
|
||||
} else {
|
||||
workItem = todo[len(todo)-1]
|
||||
}
|
||||
select {
|
||||
case workc <- workItem:
|
||||
todo = todo[:len(todo)-1]
|
||||
out++
|
||||
case it := <-w.enqueuec:
|
||||
todo = append(todo, it)
|
||||
case err := <-w.resc:
|
||||
out--
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if out == 0 && len(todo) == 0 {
|
||||
// It's safe to quit here, as long as the buffered
|
||||
// enqueue channel isn't also readable, which might
|
||||
// happen if the worker sends both another unit of
|
||||
// work and its result before the other select was
|
||||
// scheduled and both w.resc and w.enqueuec were
|
||||
// readable.
|
||||
select {
|
||||
case it := <-w.enqueuec:
|
||||
todo = append(todo, it)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doWork reads directories as instructed (via workc) and runs the
|
||||
// user's callback function.
|
||||
func (w *walker) doWork(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-w.donec:
|
||||
return
|
||||
case it := <-w.workc:
|
||||
select {
|
||||
case <-w.donec:
|
||||
return
|
||||
case w.resc <- w.walk(it.dir, !it.callbackDone):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type walker struct {
|
||||
fn func(path string, typ os.FileMode) error
|
||||
|
||||
donec chan struct{} // closed on fastWalk's return
|
||||
workc chan walkItem // to workers
|
||||
enqueuec chan walkItem // from workers
|
||||
resc chan error // from workers
|
||||
}
|
||||
|
||||
type walkItem struct {
|
||||
dir string
|
||||
callbackDone bool // callback already called; don't do it again
|
||||
}
|
||||
|
||||
func (w *walker) enqueue(it walkItem) {
|
||||
select {
|
||||
case w.enqueuec <- it:
|
||||
case <-w.donec:
|
||||
}
|
||||
}
|
||||
|
||||
func (w *walker) onDirEnt(dirName, baseName string, typ os.FileMode) error {
|
||||
joined := dirName + string(os.PathSeparator) + baseName
|
||||
if typ == os.ModeDir {
|
||||
w.enqueue(walkItem{dir: joined})
|
||||
return nil
|
||||
}
|
||||
|
||||
err := w.fn(joined, typ)
|
||||
if typ == os.ModeSymlink {
|
||||
if err == ErrTraverseLink {
|
||||
// Set callbackDone so we don't call it twice for both the
|
||||
// symlink-as-symlink and the symlink-as-directory later:
|
||||
w.enqueue(walkItem{dir: joined, callbackDone: true})
|
||||
return nil
|
||||
}
|
||||
if err == filepath.SkipDir {
|
||||
// Permit SkipDir on symlinks too.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func readDirFn(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error {
|
||||
func readDirFn(dirName string, fn func(entName string, typ os.FileMode) error) error {
|
||||
fis, err := readDir(dirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
skipFiles := false
|
||||
for _, fi := range fis {
|
||||
var mode os.FileMode
|
||||
if strings.HasSuffix(fi, SlashSeparator) {
|
||||
mode |= os.ModeDir
|
||||
}
|
||||
|
||||
if mode == 0 && skipFiles {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := fn(dirName, fi, mode); err != nil {
|
||||
if err == ErrSkipFiles {
|
||||
skipFiles = true
|
||||
continue
|
||||
}
|
||||
if err = fn(fi, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *walker) walk(root string, runUserCallback bool) error {
|
||||
if runUserCallback {
|
||||
err := w.fn(root, os.ModeDir)
|
||||
if err == filepath.SkipDir {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return readDirFn(root, w.onDirEnt)
|
||||
}
|
||||
|
||||
+90
-48
@@ -312,7 +312,7 @@ func quorumUnformattedDisks(errs []error) bool {
|
||||
}
|
||||
|
||||
// loadFormatXLAll - load all format config from all input disks in parallel.
|
||||
func loadFormatXLAll(storageDisks []StorageAPI) ([]*formatXLV3, []error) {
|
||||
func loadFormatXLAll(storageDisks []StorageAPI, heal bool) ([]*formatXLV3, []error) {
|
||||
// Initialize list of errors.
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
|
||||
@@ -331,6 +331,11 @@ func loadFormatXLAll(storageDisks []StorageAPI) ([]*formatXLV3, []error) {
|
||||
return err
|
||||
}
|
||||
formats[index] = format
|
||||
if !heal {
|
||||
// If no healing required, make the disks valid and
|
||||
// online.
|
||||
storageDisks[index].SetDiskID(format.XL.This)
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
@@ -339,7 +344,15 @@ func loadFormatXLAll(storageDisks []StorageAPI) ([]*formatXLV3, []error) {
|
||||
return formats, g.Wait()
|
||||
}
|
||||
|
||||
func saveFormatXL(disk StorageAPI, format interface{}) error {
|
||||
func saveFormatXL(disk StorageAPI, format interface{}, diskID string) error {
|
||||
if format == nil || disk == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
|
||||
if err := makeFormatXLMetaVolumes(disk); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Marshal and write to disk.
|
||||
formatBytes, err := json.Marshal(format)
|
||||
if err != nil {
|
||||
@@ -357,7 +370,12 @@ func saveFormatXL(disk StorageAPI, format interface{}) error {
|
||||
}
|
||||
|
||||
// Rename file `uuid.json` --> `format.json`.
|
||||
return disk.RenameFile(minioMetaBucket, tmpFormat, minioMetaBucket, formatConfigFile)
|
||||
if err = disk.RenameFile(minioMetaBucket, tmpFormat, minioMetaBucket, formatConfigFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
disk.SetDiskID(diskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ignoredHiddenDirectories = []string{
|
||||
@@ -426,7 +444,7 @@ func checkFormatXLValue(formatXL *formatXLV3) error {
|
||||
}
|
||||
|
||||
// Check all format values.
|
||||
func checkFormatXLValues(formats []*formatXLV3) error {
|
||||
func checkFormatXLValues(formats []*formatXLV3, drivesPerSet int) error {
|
||||
for i, formatXL := range formats {
|
||||
if formatXL == nil {
|
||||
continue
|
||||
@@ -438,6 +456,9 @@ func checkFormatXLValues(formats []*formatXLV3) error {
|
||||
return fmt.Errorf("%s disk is already being used in another erasure deployment. (Number of disks specified: %d but the number of disks found in the %s disk's format.json: %d)",
|
||||
humanize.Ordinal(i+1), len(formats), humanize.Ordinal(i+1), len(formatXL.XL.Sets)*len(formatXL.XL.Sets[0]))
|
||||
}
|
||||
if len(formatXL.XL.Sets[0]) != drivesPerSet {
|
||||
return fmt.Errorf("%s disk is already formatted with %d drives per erasure set. This cannot be changed to %d, please revert your MINIO_ERASURE_SET_DRIVE_COUNT setting", humanize.Ordinal(i+1), len(formatXL.XL.Sets[0]), drivesPerSet)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -472,7 +493,7 @@ func formatXLGetDeploymentID(refFormat *formatXLV3, formats []*formatXLV3) (stri
|
||||
func formatXLFixDeploymentID(endpoints Endpoints, storageDisks []StorageAPI, refFormat *formatXLV3) (err error) {
|
||||
// Attempt to load all `format.json` from all disks.
|
||||
var sErrs []error
|
||||
formats, sErrs := loadFormatXLAll(storageDisks)
|
||||
formats, sErrs := loadFormatXLAll(storageDisks, false)
|
||||
for i, sErr := range sErrs {
|
||||
if _, ok := formatCriticalErrors[sErr]; ok {
|
||||
return fmt.Errorf("Disk %s: %w", endpoints[i], sErr)
|
||||
@@ -516,25 +537,38 @@ func formatXLFixDeploymentID(endpoints Endpoints, storageDisks []StorageAPI, ref
|
||||
func formatXLFixLocalDeploymentID(endpoints Endpoints, storageDisks []StorageAPI, refFormat *formatXLV3) error {
|
||||
// If this server was down when the deploymentID was updated
|
||||
// then we make sure that we update the local disks with the deploymentID.
|
||||
for index, storageDisk := range storageDisks {
|
||||
if endpoints[index].IsLocal && storageDisk != nil && storageDisk.IsOnline() {
|
||||
format, err := loadFormatXL(storageDisk)
|
||||
if err != nil {
|
||||
// Disk can be offline etc.
|
||||
// ignore the errors seen here.
|
||||
continue
|
||||
}
|
||||
if format.ID != "" {
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
|
||||
continue
|
||||
}
|
||||
format.ID = refFormat.ID
|
||||
if err := saveFormatXL(storageDisk, format); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return fmt.Errorf("Unable to save format.json, %w", err)
|
||||
|
||||
// Initialize errs to collect errors inside go-routine.
|
||||
g := errgroup.WithNErrs(len(storageDisks))
|
||||
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if endpoints[index].IsLocal && storageDisks[index] != nil && storageDisks[index].IsOnline() {
|
||||
format, err := loadFormatXL(storageDisks[index])
|
||||
if err != nil {
|
||||
// Disk can be offline etc.
|
||||
// ignore the errors seen here.
|
||||
return nil
|
||||
}
|
||||
if format.ID != "" {
|
||||
return nil
|
||||
}
|
||||
if !reflect.DeepEqual(format.XL.Sets, refFormat.XL.Sets) {
|
||||
return nil
|
||||
}
|
||||
format.ID = refFormat.ID
|
||||
if err := saveFormatXL(storageDisks[index], format, format.XL.This); err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return fmt.Errorf("Unable to save format.json, %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -643,7 +677,7 @@ func initXLMetaVolumesInLocalDisks(storageDisks []StorageAPI, formats []*formatX
|
||||
// goroutine will return its own instance of index variable.
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
return makeFormatXLMetaVolumes(storageDisks[index])
|
||||
return makeFormatXLMetaVolumes(disksToInit[index])
|
||||
}, index)
|
||||
}
|
||||
|
||||
@@ -667,17 +701,11 @@ func saveFormatXLAll(ctx context.Context, storageDisks []StorageAPI, formats []*
|
||||
for index := range storageDisks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if formats[index] == nil || storageDisks[index] == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
if err := makeFormatXLMetaVolumes(storageDisks[index]); err != nil {
|
||||
return err
|
||||
}
|
||||
return saveFormatXL(storageDisks[index], formats[index])
|
||||
return saveFormatXL(storageDisks[index], formats[index], formats[index].XL.This)
|
||||
}, index)
|
||||
}
|
||||
|
||||
writeQuorum := len(storageDisks)/2 + 1
|
||||
writeQuorum := getWriteQuorum(len(storageDisks))
|
||||
// Wait for the routines to finish.
|
||||
return reduceWriteQuorumErrs(ctx, g.Wait(), nil, writeQuorum)
|
||||
}
|
||||
@@ -735,25 +763,36 @@ func formatXLV3ThisEmpty(formats []*formatXLV3) bool {
|
||||
|
||||
// fixFormatXLV3 - fix format XL configuration on all disks.
|
||||
func fixFormatXLV3(storageDisks []StorageAPI, endpoints Endpoints, formats []*formatXLV3) error {
|
||||
for i, format := range formats {
|
||||
if format == nil || !endpoints[i].IsLocal {
|
||||
continue
|
||||
}
|
||||
// NOTE: This code is specifically needed when migrating version
|
||||
// V1 to V2 to V3, in a scenario such as this we only need to handle
|
||||
// single sets since we never used to support multiple sets in releases
|
||||
// with V1 format version.
|
||||
if len(format.XL.Sets) > 1 {
|
||||
continue
|
||||
}
|
||||
if format.XL.This == "" {
|
||||
formats[i].XL.This = format.XL.Sets[0][i]
|
||||
if err := saveFormatXL(storageDisks[i], formats[i]); err != nil {
|
||||
return err
|
||||
g := errgroup.WithNErrs(len(formats))
|
||||
for i := range formats {
|
||||
i := i
|
||||
g.Go(func() error {
|
||||
if formats[i] == nil || !endpoints[i].IsLocal {
|
||||
return nil
|
||||
}
|
||||
// NOTE: This code is specifically needed when migrating version
|
||||
// V1 to V2 to V3, in a scenario such as this we only need to handle
|
||||
// single sets since we never used to support multiple sets in releases
|
||||
// with V1 format version.
|
||||
if len(formats[i].XL.Sets) > 1 {
|
||||
return nil
|
||||
}
|
||||
if formats[i].XL.This == "" {
|
||||
formats[i].XL.This = formats[i].XL.Sets[0][i]
|
||||
if err := saveFormatXL(storageDisks[i], formats[i], formats[i].XL.This); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, i)
|
||||
}
|
||||
for _, err := range g.Wait() {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// initFormatXL - save XL format configuration on all disks.
|
||||
@@ -824,8 +863,11 @@ func ecDrivesNoConfig(drivesPerSet int) int {
|
||||
|
||||
// Make XL backend meta volumes.
|
||||
func makeFormatXLMetaVolumes(disk StorageAPI) error {
|
||||
if disk == nil {
|
||||
return errDiskNotFound
|
||||
}
|
||||
// Attempt to create MinIO internal buckets.
|
||||
return disk.MakeVolBulk(minioMetaBucket, minioMetaTmpBucket, minioMetaMultipartBucket, minioMetaBackgroundOpsBucket)
|
||||
return disk.MakeVolBulk(minioMetaBucket, minioMetaTmpBucket, minioMetaMultipartBucket, dataUsageBucket)
|
||||
}
|
||||
|
||||
var initMetaVolIgnoredErrs = append(baseIgnoredErrs, errVolumeExists)
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestFixFormatV3(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newFormats, errs := loadFormatXLAll(storageDisks)
|
||||
newFormats, errs := loadFormatXLAll(storageDisks, false)
|
||||
for _, err := range errs {
|
||||
if err != nil && err != errUnformattedDisk {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -429,7 +429,7 @@ func fsDeleteFile(ctx context.Context, basePath, deletePath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteFile(basePath, deletePath); err != nil {
|
||||
if err := deleteFile(basePath, deletePath, false); err != nil {
|
||||
if err != errFileNotFound {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
@@ -182,9 +182,14 @@ func (m fsMetaV1) ToObjectInfo(bucket, object string, fi os.FileInfo) ObjectInfo
|
||||
objInfo.Expires = t.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
// Add user tags to the object info
|
||||
objInfo.UserTags = m.Meta[xhttp.AmzObjectTagging]
|
||||
|
||||
// etag/md5Sum has already been extracted. We need to
|
||||
// remove to avoid it from appearing as part of
|
||||
// response headers. e.g, X-Minio-* or X-Amz-*.
|
||||
// Tags have also been extracted, we remove that as well.
|
||||
objInfo.UserDefined = cleanMetadata(m.Meta)
|
||||
|
||||
// All the parts per object.
|
||||
|
||||
@@ -575,7 +575,6 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
|
||||
fsMeta.Parts[i] = ObjectPartInfo{
|
||||
Number: part.PartNumber,
|
||||
ETag: part.ETag,
|
||||
Size: fi.Size(),
|
||||
ActualSize: actualSize,
|
||||
}
|
||||
@@ -758,7 +757,7 @@ func (fs *FSObjects) AbortMultipartUpload(ctx context.Context, bucket, object, u
|
||||
// Removes multipart uploads if any older than `expiry` duration
|
||||
// on all buckets for every `cleanupInterval`, this function is
|
||||
// blocking and should be run in a go-routine.
|
||||
func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration, doneCh chan struct{}) {
|
||||
func (fs *FSObjects) cleanupStaleMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration, doneCh <-chan struct{}) {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
+19
-13
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -34,33 +35,38 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
obj := initFSObjects(disk, t)
|
||||
fs := obj.(*FSObjects)
|
||||
|
||||
// Close the go-routine, we are going to
|
||||
// manually start it and test in this test case.
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
bucketName := "bucket"
|
||||
objectName := "object"
|
||||
|
||||
obj.MakeBucketWithLocation(context.Background(), bucketName, "")
|
||||
uploadID, err := obj.NewMultipartUpload(context.Background(), bucketName, objectName, ObjectOptions{})
|
||||
// Create a context we can cancel.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
obj.MakeBucketWithLocation(ctx, bucketName, "")
|
||||
|
||||
uploadID, err := obj.NewMultipartUpload(ctx, bucketName, objectName, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(context.Background(), 20*time.Millisecond, 0, GlobalServiceDoneCh)
|
||||
var cleanupWg sync.WaitGroup
|
||||
cleanupWg.Add(1)
|
||||
go func() {
|
||||
defer cleanupWg.Done()
|
||||
fs.cleanupStaleMultipartUploads(context.Background(), time.Millisecond, 0, ctx.Done())
|
||||
}()
|
||||
|
||||
// Wait for 40ms such that - we have given enough time for
|
||||
// cleanup routine to kick in.
|
||||
time.Sleep(40 * time.Millisecond)
|
||||
|
||||
// Close the routine we do not need it anymore.
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
// Wait for 100ms such that - we have given enough time for
|
||||
// cleanup routine to kick in. Flaky on slow systems...
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
cancel()
|
||||
cleanupWg.Wait()
|
||||
|
||||
// Check if upload id was already purged.
|
||||
if err = obj.AbortMultipartUpload(context.Background(), bucketName, objectName, uploadID); err != nil {
|
||||
if _, ok := err.(InvalidUploadID); !ok {
|
||||
t.Fatal("Unexpected err: ", err)
|
||||
}
|
||||
} else {
|
||||
t.Error("Item was not cleaned up.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+144
-111
@@ -19,7 +19,6 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -27,7 +26,6 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -37,13 +35,16 @@ import (
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
"github.com/minio/minio/pkg/mountinfo"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
// Default etag is used for pre-existing objects.
|
||||
@@ -109,8 +110,7 @@ func initMetaVolumeFS(fsPath, fsUUID string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
metaStatsPath := pathJoin(fsPath, minioMetaBackgroundOpsBucket, fsUUID)
|
||||
if err := os.MkdirAll(metaStatsPath, 0777); err != nil {
|
||||
if err := os.MkdirAll(pathJoin(fsPath, dataUsageBucket), 0777); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -186,9 +186,9 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
}
|
||||
|
||||
// NewNSLock - initialize a new namespace RWLocker instance.
|
||||
func (fs *FSObjects) NewNSLock(ctx context.Context, bucket string, object string) RWLocker {
|
||||
func (fs *FSObjects) NewNSLock(ctx context.Context, bucket string, objects ...string) RWLocker {
|
||||
// lockers are explicitly 'nil' for FS mode since there are only local lockers
|
||||
return fs.nsMutex.NewNSLock(ctx, nil, bucket, object)
|
||||
return fs.nsMutex.NewNSLock(ctx, nil, bucket, objects...)
|
||||
}
|
||||
|
||||
// Shutdown - should be called when process shuts down.
|
||||
@@ -200,7 +200,7 @@ func (fs *FSObjects) Shutdown(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StorageInfo - returns underlying storage statistics.
|
||||
func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
||||
func (fs *FSObjects) StorageInfo(ctx context.Context, _ bool) StorageInfo {
|
||||
|
||||
atomic.AddInt64(&fs.activeIOCount, 1)
|
||||
defer func() {
|
||||
@@ -226,96 +226,44 @@ func (fs *FSObjects) StorageInfo(ctx context.Context) StorageInfo {
|
||||
return storageInfo
|
||||
}
|
||||
|
||||
func (fs *FSObjects) waitForLowActiveIO() error {
|
||||
t := time.NewTicker(lowActiveIOWaitTick)
|
||||
defer t.Stop()
|
||||
for {
|
||||
if atomic.LoadInt64(&fs.activeIOCount) >= fs.maxActiveIOCount {
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return errors.New("forced exit")
|
||||
case <-t.C:
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
func (fs *FSObjects) waitForLowActiveIO() {
|
||||
for atomic.LoadInt64(&fs.activeIOCount) >= fs.maxActiveIOCount {
|
||||
time.Sleep(lowActiveIOWaitTick)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// crawlAndGetDataUsageInfo returns data usage stats of the current FS deployment
|
||||
func (fs *FSObjects) crawlAndGetDataUsageInfo(ctx context.Context, endCh <-chan struct{}) DataUsageInfo {
|
||||
|
||||
var dataUsageInfoMu sync.Mutex
|
||||
var dataUsageInfo = DataUsageInfo{
|
||||
BucketsSizes: make(map[string]uint64),
|
||||
ObjectsSizesHistogram: make(map[string]uint64),
|
||||
// CrawlAndGetDataUsage returns data usage stats of the current FS deployment
|
||||
func (fs *FSObjects) CrawlAndGetDataUsage(ctx context.Context, updates chan<- DataUsageInfo) error {
|
||||
// Load bucket totals
|
||||
var oldCache dataUsageCache
|
||||
err := oldCache.load(ctx, fs, dataUsageCacheName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
walkFn := func(origPath string, typ os.FileMode) error {
|
||||
|
||||
select {
|
||||
case <-GlobalServiceDoneCh:
|
||||
return filepath.SkipDir
|
||||
default:
|
||||
}
|
||||
|
||||
if err := fs.waitForLowActiveIO(); err != nil {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(origPath, fs.fsPath)
|
||||
path = strings.TrimPrefix(path, SlashSeparator)
|
||||
|
||||
splits := splitN(path, SlashSeparator, 2)
|
||||
bucket := splits[0]
|
||||
prefix := splits[1]
|
||||
|
||||
if bucket == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isReservedOrInvalidBucket(bucket, false) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if prefix == "" {
|
||||
dataUsageInfoMu.Lock()
|
||||
dataUsageInfo.BucketsCount++
|
||||
dataUsageInfo.BucketsSizes[bucket] = 0
|
||||
dataUsageInfoMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if typ&os.ModeDir != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get file size
|
||||
fi, err := os.Stat(origPath)
|
||||
if oldCache.Info.Name == "" {
|
||||
oldCache.Info.Name = dataUsageRoot
|
||||
}
|
||||
buckets, err := fs.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cache, err := updateUsage(ctx, fs.fsPath, oldCache, fs.waitForLowActiveIO, func(item Item) (int64, error) {
|
||||
// Get file size, symlinks which cannot be
|
||||
// followed are automatically filtered by fastwalk.
|
||||
fi, err := os.Stat(item.Path)
|
||||
if err != nil {
|
||||
return nil
|
||||
return 0, errSkipFile
|
||||
}
|
||||
size := fi.Size()
|
||||
return fi.Size(), nil
|
||||
})
|
||||
|
||||
dataUsageInfoMu.Lock()
|
||||
dataUsageInfo.ObjectsCount++
|
||||
dataUsageInfo.ObjectsTotalSize += uint64(size)
|
||||
dataUsageInfo.BucketsSizes[bucket] += uint64(size)
|
||||
dataUsageInfo.ObjectsSizesHistogram[objSizeToHistoInterval(uint64(size))]++
|
||||
dataUsageInfoMu.Unlock()
|
||||
|
||||
return nil
|
||||
// Even if there was an error, the new cache may have better info.
|
||||
if cache.Info.LastUpdate.After(oldCache.Info.LastUpdate) {
|
||||
logger.LogIf(ctx, cache.save(ctx, fs, dataUsageCacheName))
|
||||
updates <- cache.dui(dataUsageRoot, buckets)
|
||||
}
|
||||
|
||||
fastWalk(fs.fsPath, walkFn)
|
||||
|
||||
dataUsageInfo.LastUpdate = UTCNow()
|
||||
atomic.StoreUint64(&fs.totalUsed, dataUsageInfo.ObjectsTotalSize)
|
||||
|
||||
return dataUsageInfo
|
||||
return err
|
||||
}
|
||||
|
||||
/// Bucket operations
|
||||
@@ -449,13 +397,15 @@ func (fs *FSObjects) ListBuckets(ctx context.Context) ([]BucketInfo, error) {
|
||||
|
||||
// DeleteBucket - delete a bucket and all the metadata associated
|
||||
// with the bucket including pending multipart, object metadata.
|
||||
func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
bucketLock := fs.NewNSLock(ctx, bucket, "")
|
||||
if err := bucketLock.GetLock(globalObjectTimeout); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string, forceDelete bool) error {
|
||||
if !forceDelete {
|
||||
bucketLock := fs.NewNSLock(ctx, bucket, "")
|
||||
if err := bucketLock.GetLock(globalObjectTimeout); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return err
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
}
|
||||
defer bucketLock.Unlock()
|
||||
|
||||
atomic.AddInt64(&fs.activeIOCount, 1)
|
||||
defer func() {
|
||||
@@ -467,9 +417,20 @@ func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
// Attempt to delete regular bucket.
|
||||
if err = fsRemoveDir(ctx, bucketDir); err != nil {
|
||||
return toObjectErr(err, bucket)
|
||||
if !forceDelete {
|
||||
// Attempt to delete regular bucket.
|
||||
if err = fsRemoveDir(ctx, bucketDir); err != nil {
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
} else {
|
||||
tmpBucketPath := pathJoin(fs.fsPath, minioMetaTmpBucket, bucket+"."+mustGetUUID())
|
||||
if err = fsSimpleRenameFile(ctx, bucketDir, tmpBucketPath); err != nil {
|
||||
return toObjectErr(err, bucket)
|
||||
}
|
||||
|
||||
go func() {
|
||||
fsRemoveAll(ctx, tmpBucketPath) // ignore returned error if any.
|
||||
}()
|
||||
}
|
||||
|
||||
// Cleanup all the bucket metadata.
|
||||
@@ -556,7 +517,6 @@ func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBu
|
||||
// GetObjectNInfo - returns object info and a reader for object
|
||||
// content.
|
||||
func (fs *FSObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
|
||||
if err = checkGetObjArgs(ctx, bucket, object); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1125,15 +1085,18 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) er
|
||||
// is a leaf or non-leaf entry.
|
||||
func (fs *FSObjects) listDirFactory() ListDirFunc {
|
||||
// listDir - lists all the entries at a given prefix and given entry in the prefix.
|
||||
listDir := func(bucket, prefixDir, prefixEntry string) (entries []string) {
|
||||
listDir := func(bucket, prefixDir, prefixEntry string) (emptyDir bool, entries []string) {
|
||||
var err error
|
||||
entries, err = readDir(pathJoin(fs.fsPath, bucket, prefixDir))
|
||||
if err != nil && err != errFileNotFound {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return
|
||||
return false, nil
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
sort.Strings(entries)
|
||||
return filterMatchingPrefix(entries, prefixEntry)
|
||||
return false, filterMatchingPrefix(entries, prefixEntry)
|
||||
}
|
||||
|
||||
// Return list factory instance.
|
||||
@@ -1235,6 +1198,58 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
||||
fs.listDirFactory(), fs.getObjectInfo, fs.getObjectInfo)
|
||||
}
|
||||
|
||||
// GetObjectTag - get object tags from an existing object
|
||||
func (fs *FSObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||
oi, err := fs.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
return tagging.Tagging{}, err
|
||||
}
|
||||
|
||||
tags, err := tagging.FromString(oi.UserTags)
|
||||
if err != nil {
|
||||
return tagging.Tagging{}, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// PutObjectTag - replace or add tags to an existing object
|
||||
func (fs *FSObjects) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||
fsMetaPath := pathJoin(fs.fsPath, minioMetaBucket, bucketMetaPrefix, bucket, object, fs.metaJSONFile)
|
||||
fsMeta := fsMetaV1{}
|
||||
wlk, err := fs.rwPool.Write(fsMetaPath)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
// This close will allow for locks to be synchronized on `fs.json`.
|
||||
defer wlk.Close()
|
||||
|
||||
// Read objects' metadata in `fs.json`.
|
||||
if _, err = fsMeta.ReadFrom(ctx, wlk); err != nil {
|
||||
// For any error to read fsMeta, set default ETag and proceed.
|
||||
fsMeta = fs.defaultFsJSON(object)
|
||||
}
|
||||
|
||||
// clean fsMeta.Meta of tag key, before updating the new tags
|
||||
delete(fsMeta.Meta, xhttp.AmzObjectTagging)
|
||||
|
||||
// Do not update for empty tags
|
||||
if tags != "" {
|
||||
fsMeta.Meta[xhttp.AmzObjectTagging] = tags
|
||||
}
|
||||
|
||||
if _, err = fsMeta.WriteTo(wlk); err != nil {
|
||||
return toObjectErr(err, bucket, object)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteObjectTag - delete object tags from an existing object
|
||||
func (fs *FSObjects) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||
return fs.PutObjectTag(ctx, bucket, object, "")
|
||||
}
|
||||
|
||||
// ReloadFormat - no-op for fs, Valid only for XL.
|
||||
func (fs *FSObjects) ReloadFormat(ctx context.Context, dryRun bool) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
@@ -1248,7 +1263,7 @@ func (fs *FSObjects) HealFormat(ctx context.Context, dryRun bool) (madmin.HealRe
|
||||
}
|
||||
|
||||
// HealObject - no-op for fs. Valid only for XL.
|
||||
func (fs *FSObjects) HealObject(ctx context.Context, bucket, object string, dryRun, remove bool, scanMode madmin.HealScanMode) (
|
||||
func (fs *FSObjects) HealObject(ctx context.Context, bucket, object string, opts madmin.HealOpts) (
|
||||
res madmin.HealResultItem, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return res, NotImplemented{}
|
||||
@@ -1261,8 +1276,17 @@ func (fs *FSObjects) HealBucket(ctx context.Context, bucket string, dryRun, remo
|
||||
return madmin.HealResultItem{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// Walk a bucket, optionally prefix recursively, until we have returned
|
||||
// all the content to objectInfo channel, it is callers responsibility
|
||||
// to allocate a receive channel for ObjectInfo, upon any unhandled
|
||||
// error walker returns error. Optionally if context.Done() is received
|
||||
// then Walk() stops the walker.
|
||||
func (fs *FSObjects) Walk(ctx context.Context, bucket, prefix string, results chan<- ObjectInfo) error {
|
||||
return fsWalk(ctx, fs, bucket, prefix, fs.listDirFactory(), results, fs.getObjectInfo, fs.getObjectInfo)
|
||||
}
|
||||
|
||||
// HealObjects - no-op for fs. Valid only for XL.
|
||||
func (fs *FSObjects) HealObjects(ctx context.Context, bucket, prefix string, fn func(string, string) error) (e error) {
|
||||
func (fs *FSObjects) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, fn healObjectFn) (e error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return NotImplemented{}
|
||||
}
|
||||
@@ -1273,12 +1297,6 @@ func (fs *FSObjects) ListBucketsHeal(ctx context.Context) ([]BucketInfo, error)
|
||||
return []BucketInfo{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// ListObjectsHeal - list all objects to be healed. Valid only for XL
|
||||
func (fs *FSObjects) ListObjectsHeal(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return ListObjectsInfo{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// GetMetrics - no op
|
||||
func (fs *FSObjects) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
@@ -1315,6 +1333,21 @@ func (fs *FSObjects) DeleteBucketLifecycle(ctx context.Context, bucket string) e
|
||||
return removeLifecycleConfig(ctx, fs, bucket)
|
||||
}
|
||||
|
||||
// GetBucketSSEConfig returns bucket encryption config on given bucket
|
||||
func (fs *FSObjects) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||
return getBucketSSEConfig(fs, bucket)
|
||||
}
|
||||
|
||||
// SetBucketSSEConfig sets bucket encryption config on given bucket
|
||||
func (fs *FSObjects) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error {
|
||||
return saveBucketSSEConfig(ctx, fs, bucket, config)
|
||||
}
|
||||
|
||||
// DeleteBucketSSEConfig deletes bucket encryption config on given bucket
|
||||
func (fs *FSObjects) DeleteBucketSSEConfig(ctx context.Context, bucket string) error {
|
||||
return removeBucketSSEConfig(ctx, fs, bucket)
|
||||
}
|
||||
|
||||
// ListObjectsV2 lists all blobs in bucket filtered by prefix
|
||||
func (fs *FSObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (result ListObjectsV2Info, err error) {
|
||||
marker := continuationToken
|
||||
|
||||
+7
-9
@@ -280,7 +280,7 @@ func TestFSDeleteObject(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
// Test with invalid object name
|
||||
if err := fs.DeleteObject(context.Background(), bucketName, "\\"); !isSameType(err, ObjectNameInvalid{}) {
|
||||
if err := fs.DeleteObject(context.Background(), bucketName, "\\"); !isSameType(err, ObjectNotFound{}) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
// Test with object does not exist.
|
||||
@@ -318,16 +318,16 @@ func TestFSDeleteBucket(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test with an invalid bucket name
|
||||
if err = fs.DeleteBucket(context.Background(), "fo"); !isSameType(err, BucketNotFound{}) {
|
||||
if err = fs.DeleteBucket(context.Background(), "fo", false); !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
// Test with an inexistant bucket
|
||||
if err = fs.DeleteBucket(context.Background(), "foobucket"); !isSameType(err, BucketNotFound{}) {
|
||||
if err = fs.DeleteBucket(context.Background(), "foobucket", false); !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
// Test with a valid case
|
||||
if err = fs.DeleteBucket(context.Background(), bucketName); err != nil {
|
||||
if err = fs.DeleteBucket(context.Background(), bucketName, false); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ func TestFSDeleteBucket(t *testing.T) {
|
||||
|
||||
// Delete bucket should get error disk not found.
|
||||
os.RemoveAll(disk)
|
||||
if err = fs.DeleteBucket(context.Background(), bucketName); err != nil {
|
||||
if err = fs.DeleteBucket(context.Background(), bucketName, false); err != nil {
|
||||
if !isSameType(err, BucketNotFound{}) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
@@ -356,8 +356,6 @@ func TestFSListBuckets(t *testing.T) {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
}
|
||||
|
||||
GlobalServiceDoneCh <- struct{}{}
|
||||
|
||||
// Create a bucket with invalid name
|
||||
if err := os.MkdirAll(pathJoin(fs.fsPath, "vo^"), 0777); err != nil {
|
||||
t.Fatal("Unexpected error: ", err)
|
||||
@@ -392,7 +390,7 @@ func TestFSHealObject(t *testing.T) {
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
_, err := obj.HealObject(context.Background(), "bucket", "object", false, false, madmin.HealDeepScan)
|
||||
_, err := obj.HealObject(context.Background(), "bucket", "object", madmin.HealOpts{})
|
||||
if err == nil || !isSameType(err, NotImplemented{}) {
|
||||
t.Fatalf("Heal Object should return NotImplemented error ")
|
||||
}
|
||||
@@ -404,7 +402,7 @@ func TestFSHealObjects(t *testing.T) {
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
obj := initFSObjects(disk, t)
|
||||
err := obj.HealObjects(context.Background(), "bucket", "prefix", nil)
|
||||
err := obj.HealObjects(context.Background(), "bucket", "prefix", madmin.HealOpts{}, nil)
|
||||
if err == nil || !isSameType(err, NotImplemented{}) {
|
||||
t.Fatalf("Heal Object should return NotImplemented error ")
|
||||
}
|
||||
|
||||
+39
-2
@@ -304,9 +304,12 @@ func IsBackendOnline(ctx context.Context, clnt *http.Client, urlStr string) bool
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, err = clnt.Do(req); err != nil {
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
clnt.CloseIdleConnections()
|
||||
return !xnet.IsNetworkOrHostDown(err)
|
||||
}
|
||||
xhttp.DrainBody(resp.Body)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -343,7 +346,7 @@ func ErrorRespToObjectError(err error, params ...string) error {
|
||||
err = BucketNotEmpty{}
|
||||
case "NoSuchBucketPolicy":
|
||||
err = BucketPolicyNotFound{}
|
||||
case "NoSuchBucketLifecycle":
|
||||
case "NoSuchLifecycleConfiguration":
|
||||
err = BucketLifecycleNotFound{}
|
||||
case "InvalidBucketName":
|
||||
err = BucketNameInvalid{Bucket: bucket}
|
||||
@@ -414,3 +417,37 @@ func gatewayHandleEnvVars() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldMeterRequest checks whether incoming request should be added to prometheus gateway metrics
|
||||
func shouldMeterRequest(req *http.Request) bool {
|
||||
return !(guessIsBrowserReq(req) || guessIsHealthCheckReq(req) || guessIsMetricsReq(req))
|
||||
}
|
||||
|
||||
// MetricsTransport is a custom wrapper around Transport to track metrics
|
||||
type MetricsTransport struct {
|
||||
Transport *http.Transport
|
||||
Metrics *Metrics
|
||||
}
|
||||
|
||||
// RoundTrip implements the RoundTrip method for MetricsTransport
|
||||
func (m MetricsTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
metered := shouldMeterRequest(r)
|
||||
if metered && (r.Method == http.MethodPost || r.Method == http.MethodPut) {
|
||||
m.Metrics.IncRequests(r.Method)
|
||||
if r.ContentLength > 0 {
|
||||
m.Metrics.IncBytesSent(uint64(r.ContentLength))
|
||||
}
|
||||
}
|
||||
// Make the request to the server.
|
||||
resp, err := m.Transport.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if metered && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
|
||||
m.Metrics.IncRequests(r.Method)
|
||||
if resp.ContentLength > 0 {
|
||||
m.Metrics.IncBytesReceived(uint64(resp.ContentLength))
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
+12
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2017-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,15 +35,6 @@ import (
|
||||
"github.com/minio/minio/pkg/env"
|
||||
)
|
||||
|
||||
func init() {
|
||||
logger.Init(GOPATH, GOROOT)
|
||||
logger.RegisterError(config.FmtError)
|
||||
|
||||
// Initialize globalConsoleSys system
|
||||
globalConsoleSys = NewConsoleLogger(context.Background())
|
||||
logger.AddTarget(globalConsoleSys)
|
||||
}
|
||||
|
||||
var (
|
||||
gatewayCmd = cli.Command{
|
||||
Name: "gateway",
|
||||
@@ -165,7 +156,10 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
globalServerConfig = srvCfg
|
||||
globalServerConfigMu.Unlock()
|
||||
|
||||
router := mux.NewRouter().SkipClean(true)
|
||||
// Initialize router. `SkipClean(true)` stops gorilla/mux from
|
||||
// normalizing URL path minio/minio#3256
|
||||
// avoid URL path encoding minio/minio#8950
|
||||
router := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||
|
||||
if globalEtcdClient != nil {
|
||||
// Enable STS router if etcd is enabled.
|
||||
@@ -243,12 +237,16 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
// sub-systems, make sure that we do not move the above codeblock elsewhere.
|
||||
if enableConfigOps {
|
||||
logger.FatalIf(globalConfigSys.Init(newObject), "Unable to initialize config system")
|
||||
buckets, err := newObject.ListBuckets(context.Background())
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to list buckets")
|
||||
}
|
||||
|
||||
logger.FatalIf(globalNotificationSys.Init(buckets, newObject), "Unable to initialize notification system")
|
||||
// Start watching disk for reloading config, this
|
||||
// is only enabled for "NAS" gateway.
|
||||
globalConfigSys.WatchConfigNASDisk(newObject)
|
||||
}
|
||||
|
||||
// This is only to uniquely identify each gateway deployments.
|
||||
globalDeploymentID = env.Get("MINIO_GATEWAY_DEPLOYMENT_ID", mustGetUUID())
|
||||
logger.SetDeploymentID(globalDeploymentID)
|
||||
@@ -269,7 +267,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
if globalCacheConfig.Enabled {
|
||||
// initialize the new disk cache objects.
|
||||
var cacheAPI CacheObjectLayer
|
||||
cacheAPI, err = newServerCacheObjects(context.Background(), globalCacheConfig)
|
||||
cacheAPI, err = newServerCacheObjects(GlobalContext, globalCacheConfig)
|
||||
logger.FatalIf(err, "Unable to initialize disk caching")
|
||||
|
||||
globalObjLayerMutex.Lock()
|
||||
@@ -279,7 +277,7 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
|
||||
// Populate existing buckets to the etcd backend
|
||||
if globalDNSConfig != nil {
|
||||
buckets, err := newObject.ListBuckets(context.Background())
|
||||
buckets, err := newObject.ListBuckets(GlobalContext)
|
||||
if err != nil {
|
||||
logger.Fatal(err, "Unable to list buckets")
|
||||
}
|
||||
@@ -311,8 +309,5 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
printGatewayStartupMessage(getAPIEndpoints(), gatewayName)
|
||||
}
|
||||
|
||||
// Set uptime time after object layer has initialized.
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
handleSignals()
|
||||
}
|
||||
|
||||
+10
-4
@@ -26,6 +26,8 @@ import (
|
||||
type RequestStats struct {
|
||||
Get atomic.Uint64 `json:"Get"`
|
||||
Head atomic.Uint64 `json:"Head"`
|
||||
Put atomic.Uint64 `json:"Put"`
|
||||
Post atomic.Uint64 `json:"Post"`
|
||||
}
|
||||
|
||||
// Metrics - represents bytes served from backend
|
||||
@@ -37,8 +39,8 @@ type Metrics struct {
|
||||
}
|
||||
|
||||
// IncBytesReceived - Increase total bytes received from gateway backend
|
||||
func (s *Metrics) IncBytesReceived(n int64) {
|
||||
s.bytesReceived.Add(uint64(n))
|
||||
func (s *Metrics) IncBytesReceived(n uint64) {
|
||||
s.bytesReceived.Add(n)
|
||||
}
|
||||
|
||||
// GetBytesReceived - Get total bytes received from gateway backend
|
||||
@@ -47,8 +49,8 @@ func (s *Metrics) GetBytesReceived() uint64 {
|
||||
}
|
||||
|
||||
// IncBytesSent - Increase total bytes sent to gateway backend
|
||||
func (s *Metrics) IncBytesSent(n int64) {
|
||||
s.bytesSent.Add(uint64(n))
|
||||
func (s *Metrics) IncBytesSent(n uint64) {
|
||||
s.bytesSent.Add(n)
|
||||
}
|
||||
|
||||
// GetBytesSent - Get total bytes received from gateway backend
|
||||
@@ -63,6 +65,10 @@ func (s *Metrics) IncRequests(method string) {
|
||||
s.requestStats.Get.Add(1)
|
||||
} else if method == http.MethodHead {
|
||||
s.requestStats.Head.Add(1)
|
||||
} else if method == http.MethodPut {
|
||||
s.requestStats.Put.Add(1)
|
||||
} else if method == http.MethodPost {
|
||||
s.requestStats.Post.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+55
-12
@@ -21,9 +21,13 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/lifecycle"
|
||||
|
||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
)
|
||||
|
||||
// GatewayLocker implements custom NeNSLock implementation
|
||||
@@ -33,8 +37,8 @@ type GatewayLocker struct {
|
||||
}
|
||||
|
||||
// NewNSLock - implements gateway level locker
|
||||
func (l *GatewayLocker) NewNSLock(ctx context.Context, bucket string, object string) RWLocker {
|
||||
return l.nsMutex.NewNSLock(ctx, nil, bucket, object)
|
||||
func (l *GatewayLocker) NewNSLock(ctx context.Context, bucket string, objects ...string) RWLocker {
|
||||
return l.nsMutex.NewNSLock(ctx, nil, bucket, objects...)
|
||||
}
|
||||
|
||||
// NewGatewayLayerWithLocker - initialize gateway with locker.
|
||||
@@ -45,8 +49,14 @@ func NewGatewayLayerWithLocker(gwLayer ObjectLayer) ObjectLayer {
|
||||
// GatewayUnsupported list of unsupported call stubs for gateway.
|
||||
type GatewayUnsupported struct{}
|
||||
|
||||
// CrawlAndGetDataUsage - crawl is not implemented for gateway
|
||||
func (a GatewayUnsupported) CrawlAndGetDataUsage(ctx context.Context, updates chan<- DataUsageInfo) error {
|
||||
logger.CriticalIf(ctx, errors.New("not implemented"))
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// NewNSLock is a dummy stub for gateway.
|
||||
func (a GatewayUnsupported) NewNSLock(ctx context.Context, bucket string, object string) RWLocker {
|
||||
func (a GatewayUnsupported) NewNSLock(ctx context.Context, bucket string, objects ...string) RWLocker {
|
||||
logger.CriticalIf(ctx, errors.New("not implemented"))
|
||||
return nil
|
||||
}
|
||||
@@ -121,6 +131,21 @@ func (a GatewayUnsupported) DeleteBucketLifecycle(ctx context.Context, bucket st
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// GetBucketSSEConfig returns bucket encryption config on given bucket
|
||||
func (a GatewayUnsupported) GetBucketSSEConfig(ctx context.Context, bucket string) (*bucketsse.BucketSSEConfig, error) {
|
||||
return nil, NotImplemented{}
|
||||
}
|
||||
|
||||
// SetBucketSSEConfig sets bucket encryption config on given bucket
|
||||
func (a GatewayUnsupported) SetBucketSSEConfig(ctx context.Context, bucket string, config *bucketsse.BucketSSEConfig) error {
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// DeleteBucketSSEConfig deletes bucket encryption config on given bucket
|
||||
func (a GatewayUnsupported) DeleteBucketSSEConfig(ctx context.Context, bucket string) error {
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// ReloadFormat - Not implemented stub.
|
||||
func (a GatewayUnsupported) ReloadFormat(ctx context.Context, dryRun bool) error {
|
||||
return NotImplemented{}
|
||||
@@ -141,13 +166,8 @@ func (a GatewayUnsupported) ListBucketsHeal(ctx context.Context) (buckets []Buck
|
||||
return nil, NotImplemented{}
|
||||
}
|
||||
|
||||
// ListObjectsHeal - Not implemented stub
|
||||
func (a GatewayUnsupported) ListObjectsHeal(ctx context.Context, bucket, prefix, marker, delimiter string, maxKeys int) (result ListObjectsInfo, err error) {
|
||||
return ListObjectsInfo{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// HealObject - Not implemented stub
|
||||
func (a GatewayUnsupported) HealObject(ctx context.Context, bucket, object string, dryRun, remove bool, scanMode madmin.HealScanMode) (h madmin.HealResultItem, e error) {
|
||||
func (a GatewayUnsupported) HealObject(ctx context.Context, bucket, object string, opts madmin.HealOpts) (h madmin.HealResultItem, e error) {
|
||||
return h, NotImplemented{}
|
||||
}
|
||||
|
||||
@@ -156,8 +176,13 @@ func (a GatewayUnsupported) ListObjectsV2(ctx context.Context, bucket, prefix, c
|
||||
return result, NotImplemented{}
|
||||
}
|
||||
|
||||
// Walk - Not implemented stub
|
||||
func (a GatewayUnsupported) Walk(ctx context.Context, bucket, prefix string, results chan<- ObjectInfo) error {
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// HealObjects - Not implemented stub
|
||||
func (a GatewayUnsupported) HealObjects(ctx context.Context, bucket, prefix string, fn func(string, string) error) (e error) {
|
||||
func (a GatewayUnsupported) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, fn healObjectFn) (e error) {
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
@@ -173,6 +198,24 @@ func (a GatewayUnsupported) GetMetrics(ctx context.Context) (*Metrics, error) {
|
||||
return &Metrics{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// PutObjectTag - not implemented.
|
||||
func (a GatewayUnsupported) PutObjectTag(ctx context.Context, bucket, object string, tags string) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// GetObjectTag - not implemented.
|
||||
func (a GatewayUnsupported) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return tagging.Tagging{}, NotImplemented{}
|
||||
}
|
||||
|
||||
// DeleteObjectTag - not implemented.
|
||||
func (a GatewayUnsupported) DeleteObjectTag(ctx context.Context, bucket, object string) error {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return NotImplemented{}
|
||||
}
|
||||
|
||||
// IsNotificationSupported returns whether bucket notification is applicable for this layer.
|
||||
func (a GatewayUnsupported) IsNotificationSupported() bool {
|
||||
return false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017, 2018 MinIO, Inc.
|
||||
* MinIO Cloud Storage, (C) 2017-2020 MinIO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,11 +39,10 @@ import (
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
miniogopolicy "github.com/minio/minio-go/v6/pkg/policy"
|
||||
"github.com/minio/minio/cmd"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/policy"
|
||||
"github.com/minio/minio/pkg/policy/condition"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/bucket/policy/condition"
|
||||
sha256 "github.com/minio/sha256-simd"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
@@ -94,8 +93,10 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}azureaccountkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_DRIVES{{.AssignmentOperator}}"/mnt/drive1,/mnt/drive2,/mnt/drive3,/mnt/drive4"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXCLUDE{{.AssignmentOperator}}"bucket1/*,*.png"
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_EXPIRY{{.AssignmentOperator}}40
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_QUOTA{{.AssignmentOperator}}80
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_QUOTA{{.AssignmentOperator}}90
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_AFTER{{.AssignmentOperator}}3
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_LOW{{.AssignmentOperator}}75
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_HIGH{{.AssignmentOperator}}85
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -146,7 +147,14 @@ func (g *Azure) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, erro
|
||||
return &azureObjects{}, err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: minio.NewCustomHTTPTransport()}
|
||||
metrics := minio.NewMetrics()
|
||||
|
||||
t := &minio.MetricsTransport{
|
||||
Transport: minio.NewGatewayHTTPTransport(),
|
||||
Metrics: metrics,
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: t}
|
||||
userAgent := fmt.Sprintf("APN/1.0 MinIO/1.0 MinIO/%s", minio.Version)
|
||||
|
||||
pipeline := azblob.NewPipeline(credential, azblob.PipelineOptions{
|
||||
@@ -168,6 +176,7 @@ func (g *Azure) NewGatewayLayer(creds auth.Credentials) (minio.ObjectLayer, erro
|
||||
endpoint: endpointURL.String(),
|
||||
httpClient: httpClient,
|
||||
client: client,
|
||||
metrics: metrics,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -357,6 +366,7 @@ type azureObjects struct {
|
||||
minio.GatewayUnsupported
|
||||
endpoint string
|
||||
httpClient *http.Client
|
||||
metrics *minio.Metrics
|
||||
client azblob.ServiceURL // Azure sdk client
|
||||
}
|
||||
|
||||
@@ -460,6 +470,11 @@ func parseAzurePart(metaPartFileName, prefix string) (partID int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetMetrics returns this gateway's metrics
|
||||
func (a *azureObjects) GetMetrics(ctx context.Context) (*minio.Metrics, error) {
|
||||
return a.metrics, nil
|
||||
}
|
||||
|
||||
// Shutdown - save any gateway metadata to disk
|
||||
// if necessary and reload upon next restart.
|
||||
func (a *azureObjects) Shutdown(ctx context.Context) error {
|
||||
@@ -467,7 +482,7 @@ func (a *azureObjects) Shutdown(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// StorageInfo - Not relevant to Azure backend.
|
||||
func (a *azureObjects) StorageInfo(ctx context.Context) (si minio.StorageInfo) {
|
||||
func (a *azureObjects) StorageInfo(ctx context.Context, _ bool) (si minio.StorageInfo) {
|
||||
si.Backend.Type = minio.BackendGateway
|
||||
si.Backend.GatewayOnline = minio.IsBackendOnline(ctx, a.httpClient, a.endpoint)
|
||||
return si
|
||||
@@ -544,7 +559,7 @@ func (a *azureObjects) ListBuckets(ctx context.Context) (buckets []minio.BucketI
|
||||
}
|
||||
|
||||
// DeleteBucket - delete a container on azure, uses Azure equivalent `ContainerURL.Delete`.
|
||||
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string) error {
|
||||
func (a *azureObjects) DeleteBucket(ctx context.Context, bucket string, forceDelete bool) error {
|
||||
containerURL := a.client.NewContainerURL(bucket)
|
||||
_, err := containerURL.Delete(ctx, azblob.ContainerAccessConditions{})
|
||||
return azureToObjectError(err, bucket)
|
||||
@@ -1180,7 +1195,7 @@ func (a *azureObjects) CompleteMultipartUpload(ctx context.Context, bucket, obje
|
||||
if err != nil {
|
||||
return objInfo, azureToObjectError(err, bucket, object)
|
||||
}
|
||||
objMetadata["md5sum"] = cmd.ComputeCompleteMultipartMD5(uploadedParts)
|
||||
objMetadata["md5sum"] = minio.ComputeCompleteMultipartMD5(uploadedParts)
|
||||
|
||||
_, err = objBlob.CommitBlockList(ctx, allBlocks, objProperties, objMetadata, azblob.BlobAccessConditions{})
|
||||
if err != nil {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user