mirror of
https://github.com/pgsty/minio.git
synced 2026-10-17 23:16:09 +03:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dda1fd624 | |||
| 2dc46cb153 | |||
| 0674c0075e | |||
| 518ef670da | |||
| 0dd626ec67 | |||
| 53f4c0fdc0 | |||
| 7e3ea77fdf | |||
| 7290d23b26 | |||
| 24f20eb1bd | |||
| 4c9de098b0 | |||
| a2ccba69e5 | |||
| 8eb99d3a87 | |||
| 3773874cd3 | |||
| 6c62b1a2ea | |||
| b768645fde | |||
| 7b58dcb28c | |||
| fea4a1e68e | |||
| a9e83dd42c | |||
| 145f501a21 | |||
| 9b3b04ecec | |||
| 3e063cca5c | |||
| 27d716c663 | |||
| fbd15cb7b7 | |||
| f7c91eff54 | |||
| a6bdc086a2 | |||
| 1242dd951a | |||
| d1c8e9f31b | |||
| 83ccae6c8b | |||
| 086be07bf5 | |||
| 28f9c477a8 | |||
| 09571d03a5 | |||
| 71ce63f79c | |||
| 5205c9591f | |||
| 9a547dcbfb | |||
| 27632ca6ec | |||
| c7470e6e6e | |||
| d090a17ed0 | |||
| c2529260e7 | |||
| da87188ff8 | |||
| d099039f5d | |||
| 5dd9cf4398 | |||
| ab77b216d1 | |||
| b37a02cddf | |||
| c3c3e9087b | |||
| 7ad6bc955f | |||
| 7a5271ad96 | |||
| 1b122526aa | |||
| a3b266761e | |||
| 498389123e | |||
| bc61417284 | |||
| 97d952e61c | |||
| 073aac3d92 | |||
| eff4127efd | |||
| b1c0c32ba6 | |||
| f14bf25cb9 | |||
| f216670814 | |||
| 6ecc98fddb | |||
| 4843affd0e | |||
| 558785a4bb | |||
| 60d415bb8a | |||
| 45e22cf8aa | |||
| e4900b99d7 | |||
| 20766069a8 | |||
| e8160c9fae | |||
| 957ecb1b64 | |||
| ac5061df2c | |||
| cddb2714ef | |||
| d7d9cac20b |
@@ -0,0 +1,42 @@
|
||||
name: Go
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Test on Go ${{ matrix.go-version }} and ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.13.x]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Build on ${{ matrix.os }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GO111MODULE: on
|
||||
SIMPLE_CI: 1
|
||||
run: |
|
||||
sudo apt-get install devscripts
|
||||
make
|
||||
diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
make test-race
|
||||
make crosscompile
|
||||
make verify
|
||||
make verify-healing
|
||||
@@ -29,12 +29,6 @@ matrix:
|
||||
- SIMPLE_CI=1
|
||||
go: 1.13.x
|
||||
script:
|
||||
- make
|
||||
- diff -au <(gofmt -s -d cmd) <(printf "")
|
||||
- diff -au <(gofmt -s -d pkg) <(printf "")
|
||||
- make test-race
|
||||
- make crosscompile
|
||||
- make verify
|
||||
- cd browser && npm install && npm run test && cd ..
|
||||
- bash -c 'shopt -s globstar; shellcheck mint/**/*.sh'
|
||||
|
||||
|
||||
@@ -4487,214 +4487,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/google/gofuzz
|
||||
https://github.com/google/gofuzz
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/google/martian
|
||||
https://github.com/google/martian
|
||||
----------------------------------------------------------------
|
||||
@@ -10936,25 +10728,6 @@ THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/kurin/blazer
|
||||
https://github.com/kurin/blazer
|
||||
----------------------------------------------------------------
|
||||
Copyright 2016, the Blazer authors
|
||||
|
||||
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/lib/pq
|
||||
https://github.com/lib/pq
|
||||
----------------------------------------------------------------
|
||||
@@ -16956,6 +16729,36 @@ https://github.com/soheilhy/cmux
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/spaolacci/murmur3
|
||||
https://github.com/spaolacci/murmur3
|
||||
----------------------------------------------------------------
|
||||
Copyright 2013, Sébastien Paolacci.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the library nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> 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/streadway/amqp
|
||||
https://github.com/streadway/amqp
|
||||
----------------------------------------------------------------
|
||||
@@ -17226,6 +17029,69 @@ SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/willf/bitset
|
||||
https://github.com/willf/bitset
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2014 Will Fitzgerald. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/willf/bloom
|
||||
https://github.com/willf/bloom
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2014 Will Fitzgerald. 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/xdg/scram
|
||||
https://github.com/xdg/scram
|
||||
----------------------------------------------------------------
|
||||
@@ -17899,6 +17765,31 @@ THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
go.uber.org/tools
|
||||
https://go.uber.org/tools
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2017 Uber Technologies, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
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.
|
||||
|
||||
================================================================
|
||||
|
||||
go.uber.org/zap
|
||||
https://go.uber.org/zap
|
||||
----------------------------------------------------------------
|
||||
@@ -17957,6 +17848,72 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
golang.org/x/lint
|
||||
https://golang.org/x/lint
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2013 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
golang.org/x/mod
|
||||
https://golang.org/x/mod
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
golang.org/x/net
|
||||
https://golang.org/x/net
|
||||
----------------------------------------------------------------
|
||||
@@ -18188,6 +18145,39 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
golang.org/x/xerrors
|
||||
https://golang.org/x/xerrors
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2019 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
google.golang.org/api
|
||||
https://google.golang.org/api
|
||||
----------------------------------------------------------------
|
||||
@@ -20622,3 +20612,29 @@ https://gopkg.in/yaml.v2
|
||||
|
||||
================================================================
|
||||
|
||||
honnef.co/go/tools
|
||||
https://honnef.co/go/tools
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2016 Dominik Honnef
|
||||
|
||||
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.
|
||||
|
||||
================================================================
|
||||
|
||||
|
||||
@@ -88,11 +88,6 @@ export class ChangePasswordModal extends React.Component {
|
||||
|
||||
canChangePassword() {
|
||||
const { serverInfo } = this.props
|
||||
// Password change is not allowed in WORM mode
|
||||
if (serverInfo.info.isWorm) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Password change is not allowed for temporary users(STS)
|
||||
if(serverInfo.userInfo.isTempUser) {
|
||||
return false
|
||||
|
||||
@@ -64,17 +64,6 @@ describe("ChangePasswordModal", () => {
|
||||
shallow(<ChangePasswordModal serverInfo={serverInfo} />)
|
||||
})
|
||||
|
||||
it("should not allow changing password when isWorm is true", () => {
|
||||
const newServerInfo = { ...serverInfo, info: { isWorm: true } }
|
||||
const wrapper = shallow(<ChangePasswordModal serverInfo={newServerInfo} />)
|
||||
expect(
|
||||
wrapper
|
||||
.find("ModalBody")
|
||||
.childAt(0)
|
||||
.text()
|
||||
).toBe("Credentials of this user cannot be updated through MinIO Browser.")
|
||||
})
|
||||
|
||||
it("should not allow changing password when not IAM user", () => {
|
||||
const newServerInfo = {
|
||||
...serverInfo,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { createSelector } from "reselect"
|
||||
|
||||
export const getServerInfo = state => state.browser.serverInfo
|
||||
|
||||
export const hasServerPublicDomain = createSelector(
|
||||
getServerInfo,
|
||||
serverInfo => Boolean(serverInfo.info && serverInfo.info.domains && serverInfo.info.domains.length),
|
||||
)
|
||||
@@ -77,7 +77,8 @@ export class ShareObjectModal extends React.Component {
|
||||
hideShareObject()
|
||||
}
|
||||
render() {
|
||||
const { shareObjectDetails, shareObject, hideShareObject } = this.props
|
||||
const { shareObjectDetails, hideShareObject } = this.props
|
||||
const url = `${window.location.protocol}//${shareObjectDetails.url}`
|
||||
return (
|
||||
<Modal
|
||||
show={true}
|
||||
@@ -93,11 +94,12 @@ export class ShareObjectModal extends React.Component {
|
||||
type="text"
|
||||
ref={node => (this.copyTextInput = node)}
|
||||
readOnly="readOnly"
|
||||
value={window.location.protocol + "//" + shareObjectDetails.url}
|
||||
value={url}
|
||||
onClick={() => this.copyTextInput.select()}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
{shareObjectDetails.showExpiryDate && (
|
||||
<div
|
||||
className="input-group"
|
||||
style={{ display: web.LoggedIn() ? "block" : "none" }}
|
||||
>
|
||||
@@ -174,10 +176,11 @@ export class ShareObjectModal extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ModalBody>
|
||||
<div className="modal-footer">
|
||||
<CopyToClipboard
|
||||
text={window.location.protocol + "//" + shareObjectDetails.url}
|
||||
text={url}
|
||||
onCopy={this.onUrlCopied.bind(this)}
|
||||
>
|
||||
<button className="btn btn-success">Copy Link</button>
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("ShareObjectModal", () => {
|
||||
shallow(
|
||||
<ShareObjectModal
|
||||
object={{ name: "obj1" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@@ -44,7 +44,7 @@ describe("ShareObjectModal", () => {
|
||||
const wrapper = shallow(
|
||||
<ShareObjectModal
|
||||
object={{ name: "obj1" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||
hideShareObject={hideShareObject}
|
||||
/>
|
||||
)
|
||||
@@ -59,7 +59,7 @@ describe("ShareObjectModal", () => {
|
||||
const wrapper = shallow(
|
||||
<ShareObjectModal
|
||||
object={{ name: "obj1" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||
/>
|
||||
)
|
||||
expect(
|
||||
@@ -76,7 +76,7 @@ describe("ShareObjectModal", () => {
|
||||
const wrapper = shallow(
|
||||
<ShareObjectModal
|
||||
object={{ name: "obj1" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test" }}
|
||||
shareObjectDetails={{ show: true, object: "obj1", url: "test", showExpiryDate: true }}
|
||||
hideShareObject={hideShareObject}
|
||||
showCopyAlert={showCopyAlert}
|
||||
/>
|
||||
@@ -89,8 +89,15 @@ describe("ShareObjectModal", () => {
|
||||
describe("Update expiry values", () => {
|
||||
const props = {
|
||||
object: { name: "obj1" },
|
||||
shareObjectDetails: { show: true, object: "obj1", url: "test" }
|
||||
shareObjectDetails: { show: true, object: "obj1", url: "test", showExpiryDate: true }
|
||||
}
|
||||
|
||||
it("should not show expiry values if shared with public link", () => {
|
||||
const shareObjectDetails = { show: true, object: "obj1", url: "test", showExpiryDate: false }
|
||||
const wrapper = shallow(<ShareObjectModal {...props} shareObjectDetails={shareObjectDetails} />)
|
||||
expect(wrapper.find('.set-expire').exists()).toEqual(false)
|
||||
})
|
||||
|
||||
it("should have default expiry values", () => {
|
||||
const wrapper = shallow(<ShareObjectModal {...props} />)
|
||||
expect(wrapper.state("expiry")).toEqual({
|
||||
|
||||
@@ -34,6 +34,7 @@ jest.mock("../../web", () => ({
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false),
|
||||
ListObjects: jest.fn(({ bucketName }) => {
|
||||
if (bucketName === "test-deny") {
|
||||
@@ -69,7 +70,14 @@ jest.mock("../../web", () => ({
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve({ token: "test" })
|
||||
})
|
||||
}),
|
||||
GetBucketPolicy: jest.fn(({ bucketName, prefix }) => {
|
||||
if (!bucketName) {
|
||||
return Promise.reject({ message: "Invalid bucket" })
|
||||
}
|
||||
if (bucketName === 'test-public') return Promise.resolve({ policy: 'readonly' })
|
||||
return Promise.resolve({})
|
||||
})
|
||||
}))
|
||||
|
||||
const middlewares = [thunk]
|
||||
@@ -295,7 +303,8 @@ describe("Objects actions", () => {
|
||||
type: "objects/SET_SHARE_OBJECT",
|
||||
show: true,
|
||||
object: "b.txt",
|
||||
url: "test"
|
||||
url: "test",
|
||||
showExpiryDate: true
|
||||
}
|
||||
]
|
||||
store.dispatch(actionsObjects.showShareObject("b.txt", "test"))
|
||||
@@ -321,14 +330,16 @@ describe("Objects actions", () => {
|
||||
it("creates objects/SET_SHARE_OBJECT when object is shared", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "bk1" },
|
||||
objects: { currentPrefix: "pre1/" }
|
||||
objects: { currentPrefix: "pre1/" },
|
||||
browser: { serverInfo: {} },
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SHARE_OBJECT",
|
||||
show: true,
|
||||
object: "a.txt",
|
||||
url: "https://test.com/bk1/pre1/b.txt"
|
||||
url: "https://test.com/bk1/pre1/b.txt",
|
||||
showExpiryDate: true
|
||||
},
|
||||
{
|
||||
type: "alert/SET",
|
||||
@@ -347,10 +358,42 @@ describe("Objects actions", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("creates objects/SET_SHARE_OBJECT when object is shared with public link", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "test-public" },
|
||||
objects: { currentPrefix: "pre1/" },
|
||||
browser: { serverInfo: { info: { domains: ['public.com'] }} },
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
type: "objects/SET_SHARE_OBJECT",
|
||||
show: true,
|
||||
object: "a.txt",
|
||||
url: "public.com/test-public/pre1/a.txt",
|
||||
showExpiryDate: false
|
||||
},
|
||||
{
|
||||
type: "alert/SET",
|
||||
alert: {
|
||||
type: "success",
|
||||
message: "Object shared.",
|
||||
id: alertActions.alertId
|
||||
}
|
||||
}
|
||||
]
|
||||
return store
|
||||
.dispatch(actionsObjects.shareObject("a.txt", 1, 0, 0))
|
||||
.then(() => {
|
||||
const actions = store.getActions()
|
||||
expect(actions).toEqual(expectedActions)
|
||||
})
|
||||
})
|
||||
|
||||
it("creates alert/SET when shareObject is failed", () => {
|
||||
const store = mockStore({
|
||||
buckets: { currentBucket: "" },
|
||||
objects: { currentPrefix: "pre1/" }
|
||||
objects: { currentPrefix: "pre1/" },
|
||||
browser: { serverInfo: {} },
|
||||
})
|
||||
const expectedActions = [
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import { getCurrentBucket } from "../buckets/selectors"
|
||||
import { getCurrentPrefix, getCheckedList } from "./selectors"
|
||||
import * as alertActions from "../alert/actions"
|
||||
import * as bucketActions from "../buckets/actions"
|
||||
import {
|
||||
minioBrowserPrefix,
|
||||
SORT_BY_NAME,
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
SORT_ORDER_ASC,
|
||||
SORT_ORDER_DESC,
|
||||
} from "../constants"
|
||||
import { getServerInfo, hasServerPublicDomain } from '../browser/selectors'
|
||||
|
||||
export const SET_LIST = "objects/SET_LIST"
|
||||
export const RESET_LIST = "objects/RESET_LIST"
|
||||
@@ -222,19 +222,38 @@ export const deleteCheckedObjects = () => {
|
||||
|
||||
export const shareObject = (object, days, hours, minutes) => {
|
||||
return function (dispatch, getState) {
|
||||
const hasServerDomain = hasServerPublicDomain(getState())
|
||||
const currentBucket = getCurrentBucket(getState())
|
||||
const currentPrefix = getCurrentPrefix(getState())
|
||||
const objectName = `${currentPrefix}${object}`
|
||||
const expiry = days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60
|
||||
if (web.LoggedIn()) {
|
||||
return web
|
||||
.PresignedGet({
|
||||
host: location.host,
|
||||
bucket: currentBucket,
|
||||
object: objectName,
|
||||
expiry: expiry,
|
||||
.GetBucketPolicy({ bucketName: currentBucket, prefix: currentPrefix })
|
||||
.catch(() => ({ policy: null }))
|
||||
.then(({ policy }) => {
|
||||
if (hasServerDomain && ['readonly', 'readwrite'].includes(policy)) {
|
||||
const domain = getServerInfo(getState()).info.domains[0]
|
||||
const url = `${domain}/${currentBucket}/${encodeURI(objectName)}`
|
||||
dispatch(showShareObject(object, url, false))
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
type: "success",
|
||||
message: "Object shared."
|
||||
})
|
||||
)
|
||||
} else {
|
||||
return web
|
||||
.PresignedGet({
|
||||
host: location.host,
|
||||
bucket: currentBucket,
|
||||
object: objectName,
|
||||
expiry: expiry
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((obj) => {
|
||||
if (!obj) return
|
||||
dispatch(showShareObject(object, obj.url))
|
||||
dispatch(
|
||||
alertActions.set({
|
||||
@@ -272,11 +291,12 @@ export const shareObject = (object, days, hours, minutes) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const showShareObject = (object, url) => ({
|
||||
export const showShareObject = (object, url, showExpiryDate = true) => ({
|
||||
type: SET_SHARE_OBJECT,
|
||||
show: true,
|
||||
object,
|
||||
url,
|
||||
showExpiryDate,
|
||||
})
|
||||
|
||||
export const hideShareObject = (object, url) => ({
|
||||
|
||||
@@ -89,7 +89,8 @@ export default (
|
||||
shareObject: {
|
||||
show: action.show,
|
||||
object: action.object,
|
||||
url: action.url
|
||||
url: action.url,
|
||||
showExpiryDate: action.showExpiryDate
|
||||
}
|
||||
}
|
||||
case actionsObjects.CHECKED_LIST_ADD:
|
||||
|
||||
+32
-32
File diff suppressed because one or more lines are too long
@@ -154,34 +154,6 @@ function run_test_erasure_sets() {
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure_sets_ipv6()
|
||||
{
|
||||
minio_pids=( $(start_minio_dist_erasure_sets_ipv6) )
|
||||
|
||||
export SERVER_ENDPOINT="[::1]:9000"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
for pid in "${minio_pids[@]}"; do
|
||||
kill "$pid"
|
||||
done
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
for i in $(seq 0 9); do
|
||||
echo "server$i log:"
|
||||
cat "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 0 9); do
|
||||
rm -f "$WORK_DIR/dist-minio-v6-900$i.log"
|
||||
done
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_zone_erasure_sets()
|
||||
{
|
||||
minio_pids=( $(start_minio_zone_erasure_sets) )
|
||||
|
||||
@@ -35,50 +35,45 @@ import (
|
||||
"github.com/minio/minio/cmd/config/storageclass"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) ObjectLayer {
|
||||
func validateAdminReqConfigKV(ctx context.Context, w http.ResponseWriter, r *http.Request) (auth.Credentials, ObjectLayer) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return nil
|
||||
return auth.Credentials{}, nil
|
||||
}
|
||||
|
||||
// Validate request signature.
|
||||
_, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.ConfigUpdateAdminAction, "")
|
||||
cred, adminAPIErr := checkAdminRequestAuthType(ctx, r, iampolicy.ConfigUpdateAdminAction, "")
|
||||
if adminAPIErr != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||
return nil
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
return objectAPI
|
||||
return cred, objectAPI
|
||||
}
|
||||
|
||||
// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
|
||||
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DelConfigKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
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 := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
@@ -107,24 +102,18 @@ func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
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 := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
@@ -172,7 +161,7 @@ func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -195,7 +184,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
return
|
||||
}
|
||||
|
||||
password := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, buf.Bytes())
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -208,7 +197,7 @@ func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Requ
|
||||
func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ClearConfigHistoryKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -243,7 +232,7 @@ func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *
|
||||
func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "RestoreConfigHistoryKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -289,7 +278,7 @@ func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r
|
||||
func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListConfigHistoryKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -313,7 +302,7 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
password := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
@@ -327,7 +316,7 @@ func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *h
|
||||
func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "HelpConfigKVHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
_, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -353,24 +342,18 @@ func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Req
|
||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "SetConfigHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
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 := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logger.Application)
|
||||
@@ -414,7 +397,7 @@ func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetConfigHandler")
|
||||
|
||||
objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
cred, objectAPI := validateAdminReqConfigKV(ctx, w, r)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@@ -471,7 +454,7 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
password := globalActiveCred.SecretKey
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, []byte(s.String()))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* 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 (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
bucketQuotaConfigFile = "quota.json"
|
||||
)
|
||||
|
||||
// PutBucketQuotaConfigHandler - PUT Bucket quota configuration.
|
||||
// ----------
|
||||
// Places a quota configuration on the specified bucket. The quota
|
||||
// specified in the quota configuration will be applied by default
|
||||
// to enforce total quota for the specified bucket.
|
||||
func (a adminAPIHandlers) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketQuotaConfig")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
// Turn off quota commands if data usage info is unavailable.
|
||||
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminBucketQuotaDisabled), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
quotaCfg, err := parseBucketQuota(data)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketQuotaConfigFile)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if quotaCfg.Quota > 0 {
|
||||
globalBucketQuotaSys.Set(bucket, quotaCfg)
|
||||
globalNotificationSys.PutBucketQuotaConfig(ctx, bucket, quotaCfg)
|
||||
|
||||
} else {
|
||||
globalBucketQuotaSys.Remove(bucket)
|
||||
globalNotificationSys.RemoveBucketQuotaConfig(ctx, bucket)
|
||||
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketQuotaConfigHandler - gets bucket quota configuration
|
||||
func (a adminAPIHandlers) GetBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketQuotaConfig")
|
||||
|
||||
objectAPI, _ := validateAdminUsersReq(ctx, w, r, iampolicy.GetBucketQuotaAdminAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketQuotaConfigFile)
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
if err != errConfigNotFound {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, BucketQuotaConfigNotFound{Bucket: bucket}), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, configData)
|
||||
}
|
||||
|
||||
// RemoveBucketQuotaConfigHandler - removes Bucket quota configuration.
|
||||
// ----------
|
||||
// Removes quota configuration on the specified bucket.
|
||||
func (a adminAPIHandlers) RemoveBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "RemoveBucketQuotaConfig")
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.SetBucketQuotaAdminAction)
|
||||
if objectAPI == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketQuotaConfigFile)
|
||||
if err := deleteConfig(ctx, objectAPI, configFile); err != nil {
|
||||
if err != errConfigNotFound {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, BucketQuotaConfigNotFound{Bucket: bucket}), r.URL)
|
||||
return
|
||||
}
|
||||
globalBucketQuotaSys.Remove(bucket)
|
||||
globalNotificationSys.RemoveBucketQuotaConfig(ctx, bucket)
|
||||
// Write success response.
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
+125
-69
@@ -60,12 +60,6 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
|
||||
@@ -290,12 +284,6 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
status := vars["status"]
|
||||
@@ -329,12 +317,6 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars["accessKey"]
|
||||
|
||||
@@ -397,55 +379,37 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var createReq madmin.AddServiceAccountReq
|
||||
if err = json.Unmarshal(configBytes, &createReq); err != nil {
|
||||
if err = json.Unmarshal(reqBytes, &createReq); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminConfigBadJSON, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if createReq.Parent == "" {
|
||||
apiErr := APIError{
|
||||
Code: "XMinioAdminInvalidArgument",
|
||||
Description: "Service account parent cannot be empty",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||
// Disallow creating service accounts by root user.
|
||||
if owner {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Disallow creating service accounts by root user as well.
|
||||
if createReq.Parent == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidArgument), r.URL)
|
||||
return
|
||||
parentUser := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
parentUser = cred.ParentUser
|
||||
}
|
||||
|
||||
// Disallow creating service accounts by users who are not the requested parent.
|
||||
// this restriction is not required for Owner account i.e root user.
|
||||
if createReq.Parent != cred.AccessKey && !owner {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddServiceAccountInvalidParent), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
creds, err := globalIAMSys.NewServiceAccount(ctx, createReq.Parent, createReq.Policy)
|
||||
newCred, err := globalIAMSys.NewServiceAccount(ctx, parentUser, 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) {
|
||||
// Notify all other Minio peers to reload user the service account
|
||||
for _, nerr := range globalNotificationSys.LoadServiceAccount(newCred.AccessKey) {
|
||||
if nerr.Err != nil {
|
||||
logger.GetReqInfo(ctx).SetTags("peerAddress", nerr.Host.String())
|
||||
logger.LogIf(ctx, nerr.Err)
|
||||
@@ -454,8 +418,8 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
var createResp = madmin.AddServiceAccountResp{
|
||||
Credentials: auth.Credentials{
|
||||
AccessKey: creds.AccessKey,
|
||||
SecretKey: creds.SecretKey,
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -465,13 +429,123 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
encryptedData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
// ListServiceAccounts - GET /minio/admin/v3/list-service-accounts
|
||||
func (a adminAPIHandlers) ListServiceAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ListServiceAccounts")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Disallow creating service accounts by root user.
|
||||
if owner {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
parentUser := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
parentUser = cred.ParentUser
|
||||
}
|
||||
|
||||
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, parentUser)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var listResp = madmin.ListServiceAccountsResp{
|
||||
Accounts: serviceAccounts,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(listResp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
// DeleteServiceAccount - DELETE /minio/admin/v3/delete-service-account
|
||||
func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteServiceAccount")
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil || globalIAMSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, _, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Disallow creating service accounts by root user.
|
||||
if owner {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminAccountNotEligible), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
serviceAccount := mux.Vars(r)["accessKey"]
|
||||
if serviceAccount == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := globalIAMSys.GetServiceAccountParent(ctx, serviceAccount)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
parentUser := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
parentUser = cred.ParentUser
|
||||
}
|
||||
|
||||
if parentUser != user || user == "" {
|
||||
// The service account belongs to another user but return not
|
||||
// found error to mitigate brute force attacks. or the
|
||||
// serviceAccount doesn't exist.
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServiceAccountNotFound), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessNoContent(w)
|
||||
}
|
||||
|
||||
// InfoCannedPolicyV2 - GET /minio/admin/v2/info-canned-policy?name={policyName}
|
||||
@@ -594,12 +668,6 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
||||
vars := mux.Vars(r)
|
||||
policyName := vars["name"]
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalIAMSys.DeletePolicy(policyName); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@@ -626,12 +694,6 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
vars := mux.Vars(r)
|
||||
policyName := vars["name"]
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Error out if Content-Length is missing.
|
||||
if r.ContentLength <= 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
|
||||
@@ -684,12 +746,6 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
entityName := vars["userOrGroup"]
|
||||
isGroup := vars["isGroup"] == "true"
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !isGroup {
|
||||
ok, err := globalIAMSys.IsTempUser(entityName)
|
||||
if err != nil && err != errNoSuchUser {
|
||||
|
||||
+20
-19
@@ -21,7 +21,6 @@ import (
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -44,7 +43,6 @@ import (
|
||||
"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/event/target"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
iampolicy "github.com/minio/minio/pkg/iam/policy"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
@@ -690,13 +688,15 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Define a closure to start sending whitespace to client
|
||||
// after 10s unless a response item comes in
|
||||
keepConnLive := func(w http.ResponseWriter, respCh chan healResp) {
|
||||
keepConnLive := func(w http.ResponseWriter, r *http.Request, respCh chan healResp) {
|
||||
ticker := time.NewTicker(time.Second * 10)
|
||||
defer ticker.Stop()
|
||||
started := false
|
||||
forLoop:
|
||||
for {
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if !started {
|
||||
// Start writing response to client
|
||||
@@ -792,7 +792,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
respCh <- hr
|
||||
}()
|
||||
case hip.clientToken == "":
|
||||
nh := newHealSequence(hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), numDisks, hip.hs, hip.forceStart)
|
||||
nh := newHealSequence(GlobalContext, hip.bucket, hip.objPrefix, handlers.GetSourceIP(r), numDisks, hip.hs, hip.forceStart)
|
||||
go func() {
|
||||
respBytes, apiErr, errMsg := globalAllHealState.LaunchNewHealSequence(nh)
|
||||
hr := healResp{respBytes, apiErr, errMsg}
|
||||
@@ -803,7 +803,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Due to the force-starting functionality, the Launch
|
||||
// call above can take a long time - to keep the
|
||||
// connection alive, we start sending whitespace
|
||||
keepConnLive(w, respCh)
|
||||
keepConnLive(w, r, respCh)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1534,7 +1534,6 @@ func fetchVaultStatus(cfg config.Config) madmin.Vault {
|
||||
}
|
||||
|
||||
if err := checkConnection(kmsInfo.Endpoint, 15*time.Second); err != nil {
|
||||
|
||||
vault.Status = "offline"
|
||||
} else {
|
||||
vault.Status = "online"
|
||||
@@ -1602,21 +1601,23 @@ func fetchLoggerInfo(cfg config.Config) ([]madmin.Logger, []madmin.Audit) {
|
||||
|
||||
// checkConnection - ping an endpoint , return err in case of no connection
|
||||
func checkConnection(endpointStr string, timeout time.Duration) error {
|
||||
u, pErr := xnet.ParseURL(endpointStr)
|
||||
if pErr != nil {
|
||||
return pErr
|
||||
}
|
||||
|
||||
tr := newCustomHTTPTransport(&tls.Config{RootCAs: globalRootCAs}, timeout)()
|
||||
defer tr.CloseIdleConnections()
|
||||
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) {
|
||||
return errors.New("endpoint unreachable, please check your endpoint")
|
||||
}
|
||||
}
|
||||
return dErr
|
||||
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, endpointStr, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func prepareAdminXLTestBed(ctx context.Context) (*adminXLTestBed, error) {
|
||||
|
||||
// Setup admin mgmt REST API handlers.
|
||||
adminRouter := mux.NewRouter()
|
||||
registerAdminRouter(adminRouter, true, true)
|
||||
registerAdminRouter(adminRouter, true, true, false)
|
||||
|
||||
return &adminXLTestBed{
|
||||
xlDirs: xlDirs,
|
||||
@@ -117,7 +117,7 @@ func initTestXLObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
|
||||
}
|
||||
|
||||
globalPolicySys = NewPolicySys()
|
||||
objLayer, err := newXLSets(ctx, endpoints, storageDisks, format, 1, 16)
|
||||
objLayer, err := newXLSets(ctx, endpoints, storageDisks, format)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
+102
-116
@@ -60,9 +60,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
||||
errHealPushStopNDiscard = fmt.Errorf("heal push stopped due to heal stop signal")
|
||||
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
||||
errHealIdleTimeout = fmt.Errorf("healing results were not consumed for too long")
|
||||
errHealStopSignalled = fmt.Errorf("heal stop signaled")
|
||||
|
||||
errFnHealFromAPIErr = func(ctx context.Context, err error) error {
|
||||
apiErr := toAPIError(ctx, err)
|
||||
@@ -73,10 +72,6 @@ var (
|
||||
|
||||
// healSequenceStatus - accumulated status of the heal sequence
|
||||
type healSequenceStatus struct {
|
||||
// lock to update this structure as it is concurrently
|
||||
// accessed
|
||||
updateLock *sync.RWMutex
|
||||
|
||||
// summary and detail for failures
|
||||
Summary healStatusSummary `json:"Summary"`
|
||||
FailureDetail string `json:"Detail,omitempty"`
|
||||
@@ -114,11 +109,9 @@ func initHealState() *allHealState {
|
||||
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)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-time.After(time.Minute * 5):
|
||||
now := UTCNow()
|
||||
ahs.Lock()
|
||||
for path, h := range ahs.healSeqMap {
|
||||
@@ -202,9 +195,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence) (
|
||||
existsAndLive := false
|
||||
he, exists := ahs.getHealSequence(h.path)
|
||||
if exists {
|
||||
if !he.hasEnded() || len(he.currentStatus.Items) > 0 {
|
||||
existsAndLive = true
|
||||
}
|
||||
existsAndLive = !he.hasEnded()
|
||||
}
|
||||
|
||||
if existsAndLive {
|
||||
@@ -277,8 +268,8 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
||||
}
|
||||
|
||||
// Take lock to access and update the heal-sequence
|
||||
h.currentStatus.updateLock.Lock()
|
||||
defer h.currentStatus.updateLock.Unlock()
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
numItems := len(h.currentStatus.Items)
|
||||
|
||||
@@ -289,27 +280,25 @@ func (ahs *allHealState) PopHealStatusJSON(path string,
|
||||
lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
|
||||
}
|
||||
|
||||
// After sending status to client, and before relinquishing
|
||||
// the updateLock, reset Item to nil and record the result
|
||||
// index sent to the client.
|
||||
defer func(i int64) {
|
||||
h.lastSentResultIndex = i
|
||||
h.currentStatus.Items = nil
|
||||
}(lastResultIndex)
|
||||
h.lastSentResultIndex = lastResultIndex
|
||||
|
||||
jbytes, err := json.Marshal(h.currentStatus)
|
||||
if err != nil {
|
||||
h.currentStatus.Items = nil
|
||||
|
||||
logger.LogIf(h.ctx, err)
|
||||
return nil, ErrInternalError
|
||||
}
|
||||
|
||||
h.currentStatus.Items = nil
|
||||
|
||||
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
|
||||
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
|
||||
@@ -321,9 +310,12 @@ type healSequence struct {
|
||||
// path is just pathJoin(bucket, objPrefix)
|
||||
path string
|
||||
|
||||
// List of entities (format, buckets, objects) to heal
|
||||
// A channel of entities (format, buckets, objects) to heal
|
||||
sourceCh chan healSource
|
||||
|
||||
// A channel of entities with heal result
|
||||
respCh chan healResult
|
||||
|
||||
// Report healing progress
|
||||
reportProgress bool
|
||||
|
||||
@@ -349,9 +341,8 @@ type healSequence struct {
|
||||
// completed
|
||||
traverseAndHealDoneCh chan error
|
||||
|
||||
// channel to signal heal sequence to stop (e.g. from the
|
||||
// heal-stop API)
|
||||
stopSignalCh chan struct{}
|
||||
// canceler to cancel heal sequence.
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
// the last result index sent to client
|
||||
lastSentResultIndex int64
|
||||
@@ -377,14 +368,15 @@ type healSequence struct {
|
||||
|
||||
// NewHealSequence - creates healSettings, assumes bucket and
|
||||
// objPrefix are already validated.
|
||||
func newHealSequence(bucket, objPrefix, clientAddr string,
|
||||
func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
||||
numDisks int, hs madmin.HealOpts, forceStart bool) *healSequence {
|
||||
|
||||
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
||||
reqInfo.AppendTags("prefix", objPrefix)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
|
||||
|
||||
return &healSequence{
|
||||
respCh: make(chan healResult),
|
||||
bucket: bucket,
|
||||
objPrefix: objPrefix,
|
||||
path: pathJoin(bucket, objPrefix),
|
||||
@@ -398,10 +390,9 @@ func newHealSequence(bucket, objPrefix, clientAddr string,
|
||||
Summary: healNotStartedStatus,
|
||||
HealSettings: hs,
|
||||
NumDisks: numDisks,
|
||||
updateLock: &sync.RWMutex{},
|
||||
},
|
||||
traverseAndHealDoneCh: make(chan error),
|
||||
stopSignalCh: make(chan struct{}),
|
||||
cancelCtx: cancel,
|
||||
ctx: ctx,
|
||||
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
@@ -484,7 +475,7 @@ func (h *healSequence) gethealFailedItemsMap() map[string]int64 {
|
||||
// external signal)
|
||||
func (h *healSequence) isQuitting() bool {
|
||||
select {
|
||||
case <-h.stopSignalCh:
|
||||
case <-h.ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -493,19 +484,15 @@ func (h *healSequence) isQuitting() bool {
|
||||
|
||||
// check if the heal sequence has ended
|
||||
func (h *healSequence) hasEnded() bool {
|
||||
h.currentStatus.updateLock.RLock()
|
||||
summary := h.currentStatus.Summary
|
||||
h.currentStatus.updateLock.RUnlock()
|
||||
return summary == healStoppedStatus || summary == healFinishedStatus
|
||||
h.mutex.RLock()
|
||||
ended := len(h.currentStatus.Items) == 0 || h.currentStatus.Summary == healStoppedStatus || h.currentStatus.Summary == healFinishedStatus
|
||||
h.mutex.RUnlock()
|
||||
return ended
|
||||
}
|
||||
|
||||
// stops the heal sequence - safe to call multiple times.
|
||||
func (h *healSequence) stop() {
|
||||
select {
|
||||
case <-h.stopSignalCh:
|
||||
default:
|
||||
close(h.stopSignalCh)
|
||||
}
|
||||
h.cancelCtx()
|
||||
}
|
||||
|
||||
// pushHealResultItem - pushes a heal result item for consumption in
|
||||
@@ -532,29 +519,27 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
|
||||
var itemsLen int
|
||||
for {
|
||||
h.currentStatus.updateLock.Lock()
|
||||
h.mutex.Lock()
|
||||
itemsLen = len(h.currentStatus.Items)
|
||||
if itemsLen == maxUnconsumedHealResultItems {
|
||||
// unlock and wait to check again if we can push
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// wait for a second, or quit if an external
|
||||
// stop signal is received or the
|
||||
// unconsumedTimer fires.
|
||||
select {
|
||||
// Check after a second
|
||||
case <-time.After(time.Second):
|
||||
h.mutex.Unlock()
|
||||
continue
|
||||
|
||||
case <-h.stopSignalCh:
|
||||
case <-h.ctx.Done():
|
||||
h.mutex.Unlock()
|
||||
// discard result and return.
|
||||
return errHealPushStopNDiscard
|
||||
return errHealStopSignalled
|
||||
|
||||
// Timeout if no results consumed for too
|
||||
// long.
|
||||
// Timeout if no results consumed for too long.
|
||||
case <-unconsumedTimer.C:
|
||||
h.mutex.Unlock()
|
||||
return errHealIdleTimeout
|
||||
|
||||
}
|
||||
}
|
||||
break
|
||||
@@ -571,13 +556,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
h.currentStatus.Items = append(h.currentStatus.Items, r)
|
||||
|
||||
// release lock
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
|
||||
// This is a "safe" point for the heal sequence to quit if
|
||||
// signaled externally.
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -591,10 +570,10 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
// the heal-sequence.
|
||||
func (h *healSequence) healSequenceStart() {
|
||||
// Set status as running
|
||||
h.currentStatus.updateLock.Lock()
|
||||
h.mutex.Lock()
|
||||
h.currentStatus.Summary = healRunningStatus
|
||||
h.currentStatus.StartTime = UTCNow()
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
h.mutex.Unlock()
|
||||
|
||||
if h.sourceCh == nil {
|
||||
go h.traverseAndHeal()
|
||||
@@ -604,25 +583,27 @@ func (h *healSequence) healSequenceStart() {
|
||||
|
||||
select {
|
||||
case err, ok := <-h.traverseAndHealDoneCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.mutex.Lock()
|
||||
h.endTime = UTCNow()
|
||||
h.currentStatus.updateLock.Lock()
|
||||
defer h.currentStatus.updateLock.Unlock()
|
||||
// Heal traversal is complete.
|
||||
if ok {
|
||||
if err == nil {
|
||||
// heal traversal succeeded.
|
||||
h.currentStatus.Summary = healFinishedStatus
|
||||
} else {
|
||||
// heal traversal had an error.
|
||||
h.currentStatus.Summary = healStoppedStatus
|
||||
h.currentStatus.FailureDetail = err.Error()
|
||||
} else {
|
||||
// heal traversal succeeded.
|
||||
h.currentStatus.Summary = healFinishedStatus
|
||||
}
|
||||
|
||||
case <-h.stopSignalCh:
|
||||
h.mutex.Unlock()
|
||||
case <-h.ctx.Done():
|
||||
h.mutex.Lock()
|
||||
h.endTime = UTCNow()
|
||||
h.currentStatus.updateLock.Lock()
|
||||
h.currentStatus.Summary = healStoppedStatus
|
||||
h.currentStatus.FailureDetail = errHealStopSignalled.Error()
|
||||
h.currentStatus.updateLock.Unlock()
|
||||
h.mutex.Unlock()
|
||||
|
||||
// drain traverse channel so the traversal
|
||||
// go-routine does not leak.
|
||||
@@ -636,53 +617,64 @@ func (h *healSequence) healSequenceStart() {
|
||||
}
|
||||
|
||||
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
||||
var respCh = make(chan healResult)
|
||||
defer close(respCh)
|
||||
// Send heal request
|
||||
task := healTask{
|
||||
path: source.path,
|
||||
responseCh: respCh,
|
||||
opts: h.settings,
|
||||
responseCh: h.respCh,
|
||||
}
|
||||
if source.opts != nil {
|
||||
task.opts = *source.opts
|
||||
if !source.opts.Equal(h.settings) {
|
||||
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]++
|
||||
select {
|
||||
case res := <-h.respCh:
|
||||
if !h.reportProgress {
|
||||
// Object might have been deleted, by the time heal
|
||||
// was attempted, we should ignore this object and
|
||||
// return success.
|
||||
if isErrObjectNotFound(res.err) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// Only object type reported for successful healing
|
||||
h.healedItemsMap[res.result.Type]++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
res.result.Type = healType
|
||||
if res.err != nil {
|
||||
// Object might have been deleted, by the time heal
|
||||
// was attempted, we should ignore this object and return success.
|
||||
if isErrObjectNotFound(res.err) {
|
||||
return nil
|
||||
}
|
||||
// Only report object error
|
||||
if healType != madmin.HealItemObject {
|
||||
|
||||
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 {
|
||||
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]++
|
||||
}
|
||||
|
||||
// Report caller of any failure
|
||||
return res.err
|
||||
}
|
||||
res.result.Detail = res.err.Error()
|
||||
res.result.Type = healType
|
||||
if res.err != nil {
|
||||
// Object might have been deleted, by the time heal
|
||||
// was attempted, we should ignore this object and return success.
|
||||
if isErrObjectNotFound(res.err) {
|
||||
return nil
|
||||
}
|
||||
// Only report object error
|
||||
if healType != madmin.HealItemObject {
|
||||
return res.err
|
||||
}
|
||||
res.result.Detail = res.err.Error()
|
||||
}
|
||||
return h.pushHealResultItem(res.result)
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
return h.pushHealResultItem(res.result)
|
||||
}
|
||||
|
||||
func (h *healSequence) healItemsFromSourceCh() error {
|
||||
@@ -693,7 +685,10 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
||||
|
||||
for {
|
||||
select {
|
||||
case source := <-h.sourceCh:
|
||||
case source, ok := <-h.sourceCh:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var itemType madmin.HealItemType
|
||||
switch {
|
||||
case source.path == nopHeal:
|
||||
@@ -712,8 +707,6 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
||||
|
||||
h.scannedItemsMap[itemType]++
|
||||
h.lastHealActivity = UTCNow()
|
||||
case <-h.traverseAndHealDoneCh:
|
||||
return nil
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
@@ -722,7 +715,6 @@ func (h *healSequence) healItemsFromSourceCh() error {
|
||||
|
||||
func (h *healSequence) healFromSourceCh() {
|
||||
h.healItemsFromSourceCh()
|
||||
close(h.traverseAndHealDoneCh)
|
||||
}
|
||||
|
||||
func (h *healSequence) healItems(bucketsOnly bool) error {
|
||||
@@ -754,13 +746,7 @@ func (h *healSequence) healItems(bucketsOnly bool) error {
|
||||
// two objects.
|
||||
func (h *healSequence) traverseAndHeal() {
|
||||
bucketsOnly := false // Heals buckets and objects also.
|
||||
if err := h.healItems(bucketsOnly); err != nil {
|
||||
if h.isQuitting() {
|
||||
err = errHealStopSignalled
|
||||
}
|
||||
h.traverseAndHealDoneCh <- err
|
||||
}
|
||||
|
||||
h.traverseAndHealDoneCh <- h.healItems(bucketsOnly)
|
||||
close(h.traverseAndHealDoneCh)
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -35,7 +35,7 @@ const (
|
||||
type adminAPIHandlers struct{}
|
||||
|
||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) {
|
||||
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps, enableBucketQuotaOps bool) {
|
||||
|
||||
adminAPI := adminAPIHandlers{}
|
||||
// Admin router
|
||||
@@ -121,6 +121,8 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
|
||||
// Service accounts ops
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(httpTraceHdrs(adminAPI.AddServiceAccount))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(httpTraceHdrs(adminAPI.ListServiceAccounts))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(httpTraceHdrs(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
if adminVersion == adminAPIVersionV2Prefix {
|
||||
// Info policy IAM v2
|
||||
@@ -164,7 +166,19 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)
|
||||
|
||||
// Set Group Status
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(httpTraceHdrs(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||
}
|
||||
|
||||
// Quota operations
|
||||
if enableConfigOps && enableBucketQuotaOps {
|
||||
// GetBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||
httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||
// PutBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||
httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||
// RemoveBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-bucket-quota").HandlerFunc(
|
||||
httpTraceHdrs(adminAPI.RemoveBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||
}
|
||||
|
||||
// -- Top APIs --
|
||||
|
||||
+39
-11
@@ -28,13 +28,13 @@ import (
|
||||
"google.golang.org/api/googleapi"
|
||||
|
||||
minio "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||
"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"
|
||||
@@ -157,6 +157,7 @@ const (
|
||||
ErrInvalidRetentionDate
|
||||
ErrPastObjectLockRetainDate
|
||||
ErrUnknownWORMModeDirective
|
||||
ErrBucketTaggingNotFound
|
||||
ErrObjectLockInvalidHeaders
|
||||
ErrInvalidTagDirective
|
||||
// Add new error codes here.
|
||||
@@ -235,6 +236,10 @@ const (
|
||||
ErrAdminCredentialsMismatch
|
||||
ErrInsecureClientRequest
|
||||
ErrObjectTampered
|
||||
// Bucket Quota error codes
|
||||
ErrAdminBucketQuotaExceeded
|
||||
ErrAdminNoSuchQuotaConfiguration
|
||||
ErrAdminBucketQuotaDisabled
|
||||
|
||||
ErrHealNotImplemented
|
||||
ErrHealNoSuchProcess
|
||||
@@ -334,8 +339,8 @@ const (
|
||||
ErrAdminProfilerNotEnabled
|
||||
ErrInvalidDecompressedSize
|
||||
ErrAddUserInvalidArgument
|
||||
ErrAddServiceAccountInvalidArgument
|
||||
ErrAddServiceAccountInvalidParent
|
||||
ErrAdminAccountNotEligible
|
||||
ErrServiceAccountNotFound
|
||||
ErrPostPolicyConditionInvalidFormat
|
||||
)
|
||||
|
||||
@@ -773,6 +778,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Bucket is missing ObjectLockConfiguration",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBucketTaggingNotFound: {
|
||||
Code: "NoSuchTagSet",
|
||||
Description: "The TagSet does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrObjectLockConfigurationNotFound: {
|
||||
Code: "ObjectLockConfigurationNotFoundError",
|
||||
Description: "Object Lock configuration does not exist for this bucket",
|
||||
@@ -1089,6 +1099,21 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Credentials in config mismatch with server environment variables",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
ErrAdminBucketQuotaExceeded: {
|
||||
Code: "XMinioAdminBucketQuotaExceeded",
|
||||
Description: "Bucket quota exceeded",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminNoSuchQuotaConfiguration: {
|
||||
Code: "XMinioAdminNoSuchQuotaConfiguration",
|
||||
Description: "The quota configuration does not exist",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
ErrAdminBucketQuotaDisabled: {
|
||||
Code: "XMinioAdminBucketQuotaDisabled",
|
||||
Description: "Quota specified but disk usage crawl is disabled on MinIO server",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInsecureClientRequest: {
|
||||
Code: "XMinioInsecureClientRequest",
|
||||
Description: "Cannot respond to plain-text request from TLS-encrypted server",
|
||||
@@ -1599,17 +1624,16 @@ var errorCodes = errorCodeMap{
|
||||
Description: "User is not allowed to be same as admin access key",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAddServiceAccountInvalidArgument: {
|
||||
ErrAdminAccountNotEligible: {
|
||||
Code: "XMinioInvalidIAMCredentials",
|
||||
Description: "Creating service accounts for admin access key is not allowed",
|
||||
Description: "The administrator key is not eligible for this operation",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
ErrAddServiceAccountInvalidParent: {
|
||||
Code: "XMinioInvalidIAMCredentialsParent",
|
||||
Description: "Creating service accounts for other users is not allowed",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
ErrServiceAccountNotFound: {
|
||||
Code: "XMinioInvalidIAMCredentials",
|
||||
Description: "The specified service account is not found",
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
},
|
||||
|
||||
ErrPostPolicyConditionInvalidFormat: {
|
||||
Code: "PostPolicyInvalidKeyName",
|
||||
Description: "Invalid according to Policy: Policy Condition failed",
|
||||
@@ -1784,6 +1808,10 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
||||
apiErr = ErrNoSuchLifecycleConfiguration
|
||||
case BucketSSEConfigNotFound:
|
||||
apiErr = ErrNoSuchBucketSSEConfig
|
||||
case BucketQuotaConfigNotFound:
|
||||
apiErr = ErrAdminNoSuchQuotaConfiguration
|
||||
case BucketQuotaExceeded:
|
||||
apiErr = ErrAdminBucketQuotaExceeded
|
||||
case *event.ErrInvalidEventName:
|
||||
apiErr = ErrEventNotification
|
||||
case *event.ErrInvalidARN:
|
||||
@@ -1874,7 +1902,7 @@ func toAPIError(ctx context.Context, err error) APIError {
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case tagging.Error:
|
||||
case tags.Error:
|
||||
apiErr = APIError{
|
||||
Code: e.Code(),
|
||||
Description: e.Error(),
|
||||
|
||||
+2
-1
@@ -601,7 +601,8 @@ func generateCompleteMultpartUploadResponse(bucket, key, location, etag string)
|
||||
Location: location,
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ETag: etag,
|
||||
// AWS S3 quotes the ETag in XML, make sure we are compatible here.
|
||||
ETag: "\"" + etag + "\"",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -194,7 +194,7 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
|
||||
// GetBucketReplicationHandler - this is a dummy call.
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbucketreplication", httpTraceAll(api.GetBucketReplicationHandler)))).Queries("replication", "")
|
||||
// GetBucketTaggingHandler - this is a dummy call.
|
||||
// GetBucketTaggingHandler
|
||||
bucket.Methods(http.MethodGet).HandlerFunc(
|
||||
maxClients(collectAPIStats("getbuckettagging", httpTraceAll(api.GetBucketTaggingHandler)))).Queries("tagging", "")
|
||||
//DeleteBucketWebsiteHandler
|
||||
@@ -244,6 +244,9 @@ func registerAPIRouter(router *mux.Router, encryptionEnabled, allowSSEKMS bool)
|
||||
// PutBucketObjectLockConfig
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketobjectlockconfig", httpTraceAll(api.PutBucketObjectLockConfigHandler)))).Queries("object-lock", "")
|
||||
// PutBucketTaggingHandler
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbuckettagging", httpTraceAll(api.PutBucketTaggingHandler)))).Queries("tagging", "")
|
||||
// PutBucketVersioning
|
||||
bucket.Methods(http.MethodPut).HandlerFunc(
|
||||
maxClients(collectAPIStats("putbucketversioning", httpTraceAll(api.PutBucketVersioningHandler)))).Queries("versioning", "")
|
||||
|
||||
@@ -88,9 +88,10 @@ func (h *healRoutine) run(ctx context.Context, objAPI ObjectLayer) {
|
||||
case bucket != "" && object != "":
|
||||
res, err = objAPI.HealObject(ctx, bucket, object, task.opts)
|
||||
}
|
||||
if task.responseCh != nil {
|
||||
task.responseCh <- healResult{result: res, err: err}
|
||||
if task.path != slashSeparator && task.path != nopHeal {
|
||||
ObjectPathUpdated(task.path)
|
||||
}
|
||||
task.responseCh <- healResult{result: res, err: err}
|
||||
case <-h.doneCh:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -57,6 +57,7 @@ func monitorLocalDisksAndHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
case <-time.After(defaultMonitorNewDiskInterval):
|
||||
// Attempt a heal as the server starts-up first.
|
||||
localDisksInZoneHeal := make([]Endpoints, len(z.zones))
|
||||
var healNewDisks bool
|
||||
for i, ep := range globalEndpoints {
|
||||
localDisksToHeal := Endpoints{}
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
@@ -74,6 +75,12 @@ func monitorLocalDisksAndHeal(ctx context.Context, objAPI ObjectLayer) {
|
||||
continue
|
||||
}
|
||||
localDisksInZoneHeal[i] = localDisksToHeal
|
||||
healNewDisks = true
|
||||
}
|
||||
|
||||
// Reformat disks only if needed.
|
||||
if !healNewDisks {
|
||||
continue
|
||||
}
|
||||
|
||||
// Reformat disks
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
@@ -69,7 +70,12 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
// 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))
|
||||
apiErr := APIError{
|
||||
Code: "MalformedXML",
|
||||
Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, err),
|
||||
HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode,
|
||||
}
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,7 +92,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Update the in-memory bucket encryption cache
|
||||
globalBucketSSEConfigSys.Set(bucket, *encConfig)
|
||||
globalBucketSSEConfigSys.Set(bucket, encConfig)
|
||||
|
||||
// Update peer MinIO servers of the updated bucket encryption config
|
||||
globalNotificationSys.SetBucketSSEConfig(ctx, bucket, encConfig)
|
||||
@@ -174,6 +180,7 @@ func (api objectAPIHandlers) DeleteBucketEncryptionHandler(w http.ResponseWriter
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
+11
-10
@@ -31,13 +31,13 @@ import (
|
||||
// BucketSSEConfigSys - in-memory cache of bucket encryption config
|
||||
type BucketSSEConfigSys struct {
|
||||
sync.RWMutex
|
||||
bucketSSEConfigMap map[string]bucketsse.BucketSSEConfig
|
||||
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),
|
||||
bucketSSEConfigMap: make(map[string]*bucketsse.BucketSSEConfig),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,12 @@ func (sys *BucketSSEConfigSys) load(buckets []BucketInfo, objAPI ObjectLayer) er
|
||||
config, err := objAPI.GetBucketSSEConfig(GlobalContext, bucket.Name)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketSSEConfigNotFound); ok {
|
||||
sys.Remove(bucket.Name)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
// Quorum errors should be returned.
|
||||
return err
|
||||
}
|
||||
sys.Set(bucket.Name, *config)
|
||||
sys.Set(bucket.Name, config)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -73,7 +74,7 @@ func (sys *BucketSSEConfigSys) Init(buckets []BucketInfo, objAPI ObjectLayer) er
|
||||
}
|
||||
|
||||
// Get - gets bucket encryption config for the given bucket.
|
||||
func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSEConfig, ok bool) {
|
||||
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()
|
||||
@@ -85,7 +86,7 @@ func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSECon
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return *cfg, true
|
||||
return cfg, true
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
@@ -95,7 +96,7 @@ func (sys *BucketSSEConfigSys) Get(bucket string) (config bucketsse.BucketSSECon
|
||||
}
|
||||
|
||||
// Set - sets bucket encryption config to given bucket name.
|
||||
func (sys *BucketSSEConfigSys) Set(bucket string, config bucketsse.BucketSSEConfig) {
|
||||
func (sys *BucketSSEConfigSys) Set(bucket string, config *bucketsse.BucketSSEConfig) {
|
||||
// We don't cache bucket encryption config in gateway mode.
|
||||
if globalIsGateway {
|
||||
return
|
||||
@@ -146,8 +147,8 @@ func removeBucketSSEConfig(ctx context.Context, objAPI ObjectLayer, bucket strin
|
||||
// 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 {
|
||||
if err := deleteConfig(ctx, objAPI, configFile); err != nil {
|
||||
if err == errConfigNotFound {
|
||||
return BucketSSEConfigNotFound{Bucket: bucket}
|
||||
}
|
||||
return err
|
||||
|
||||
+165
-55
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
@@ -44,10 +45,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`
|
||||
objectLockConfig = "object-lock.xml"
|
||||
bucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
|
||||
bucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
|
||||
getBucketVersioningResponse = `<VersioningConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"/>`
|
||||
objectLockConfig = "object-lock.xml"
|
||||
bucketTaggingConfigFile = "tagging.xml"
|
||||
)
|
||||
|
||||
// Check if there are buckets on server without corresponding entry in etcd backend and
|
||||
@@ -292,6 +292,16 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
for i := range bucketsInfo {
|
||||
meta, err := loadBucketMetadata(ctx, objectAPI, bucketsInfo[i].Name)
|
||||
if err == nil {
|
||||
bucketsInfo[i].Created = meta.Created
|
||||
}
|
||||
if err != errMetaDataConverted {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s3Error == ErrAccessDenied {
|
||||
@@ -532,12 +542,11 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if objectLockEnabled {
|
||||
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
|
||||
}
|
||||
meta := newBucketMetadata(bucket)
|
||||
meta.LockEnabled = objectLockEnabled
|
||||
if err := meta.save(ctx, objectAPI); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if err = globalDNSConfig.Put(bucket); err != nil {
|
||||
@@ -576,14 +585,18 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if objectLockEnabled && !globalIsGateway {
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, []byte(bucketObjectLockEnabledConfig)); err != nil {
|
||||
if !globalIsGateway {
|
||||
meta := newBucketMetadata(bucket)
|
||||
meta.LockEnabled = objectLockEnabled
|
||||
if err := meta.save(ctx, objectAPI); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
globalBucketObjectLockConfig.Set(bucket, objectlock.Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, objectlock.Retention{})
|
||||
if objectLockEnabled {
|
||||
ret := &objectlock.Retention{}
|
||||
globalBucketObjectLockConfig.Set(bucket, ret)
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, ret)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure to add Location information here only for bucket
|
||||
@@ -619,7 +632,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
||||
bucket := mux.Vars(r)["bucket"]
|
||||
|
||||
// To detect if the client has disconnected.
|
||||
r.Body = &detectDisconnect{r.Body, r.Context().Done()}
|
||||
r.Body = &contextReader{r.Body, r.Context()}
|
||||
|
||||
// Require Content-Length to be set in the request
|
||||
size := r.ContentLength
|
||||
@@ -920,7 +933,7 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := globalBucketObjectLockConfig.Get(bucket); (ok || globalWORMEnabled) && forceDelete {
|
||||
if _, ok := globalBucketObjectLockConfig.Get(bucket); ok && forceDelete {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
@@ -942,6 +955,9 @@ func (api objectAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
// Delete metadata, only log errors.
|
||||
logger.LogIf(ctx, newBucketMetadata(bucket).delete(ctx, objectAPI))
|
||||
|
||||
globalNotificationSys.DeleteBucket(ctx, bucket)
|
||||
|
||||
// Write success response.
|
||||
@@ -1021,11 +1037,6 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketObjectLockConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1037,27 +1048,23 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
|
||||
meta, err := loadBucketMetadata(ctx, objectAPI, bucket)
|
||||
if err != nil {
|
||||
aerr := toAPIError(ctx, err)
|
||||
if err == errConfigNotFound {
|
||||
aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed)
|
||||
}
|
||||
writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !meta.LockEnabled {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if string(configData) != bucketObjectLockEnabledConfig {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
data, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -1067,8 +1074,8 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
globalBucketObjectLockConfig.Set(bucket, retention)
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, retention)
|
||||
} else {
|
||||
globalBucketObjectLockConfig.Remove(bucket)
|
||||
globalNotificationSys.RemoveBucketObjectLockConfig(ctx, bucket)
|
||||
globalBucketObjectLockConfig.Set(bucket, &objectlock.Retention{})
|
||||
globalNotificationSys.PutBucketObjectLockConfig(ctx, bucket, &objectlock.Retention{})
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
@@ -1098,26 +1105,19 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketObjectLockEnabledConfigFile)
|
||||
|
||||
meta, err := loadBucketMetadata(ctx, objectAPI, bucket)
|
||||
if err != nil && err != errMetaDataConverted {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if !meta.LockEnabled {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLockConfigurationNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
var aerr APIError
|
||||
if err == errConfigNotFound {
|
||||
aerr = errorCodes.ToAPIErr(ErrObjectLockConfigurationNotFound)
|
||||
} else {
|
||||
aerr = toAPIError(ctx, err)
|
||||
}
|
||||
writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if string(configData) != bucketObjectLockEnabledConfig {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInternalError), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
configFile = path.Join(bucketConfigPrefix, bucket, objectLockConfig)
|
||||
configData, err = readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
if err != errConfigNotFound {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
@@ -1133,3 +1133,113 @@ func (api objectAPIHandlers) GetBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, configData)
|
||||
}
|
||||
|
||||
// PutBucketTaggingHandler - PUT Bucket tagging.
|
||||
// ----------
|
||||
func (api objectAPIHandlers) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketTagging")
|
||||
|
||||
defer logger.AuditLog(w, r, "PutBucketTagging", mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
tags, err := tags.ParseBucketXML(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
data, err := xml.Marshal(tags)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketTaggingConfigFile)
|
||||
if err = saveConfig(ctx, objectAPI, configFile, data); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketTaggingHandler - GET Bucket tagging.
|
||||
// ----------
|
||||
func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketTagging")
|
||||
|
||||
defer logger.AuditLog(w, r, "GetBucketTagging", mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketTaggingConfigFile)
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
var aerr APIError
|
||||
if err == errConfigNotFound {
|
||||
aerr = errorCodes.ToAPIErr(ErrBucketTaggingNotFound)
|
||||
} else {
|
||||
aerr = toAPIError(ctx, err)
|
||||
}
|
||||
writeErrorResponse(ctx, w, aerr, r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, configData)
|
||||
}
|
||||
|
||||
// DeleteBucketTaggingHandler - DELETE Bucket tagging.
|
||||
// ----------
|
||||
func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteBucketTagging")
|
||||
|
||||
defer logger.AuditLog(w, r, "DeleteBucketTagging", mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketTaggingAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
configFile := path.Join(bucketConfigPrefix, bucket, bucketTaggingConfigFile)
|
||||
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && err != errConfigNotFound {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
globalLifecycleSys.Set(bucket, *bucketLifecycle)
|
||||
globalLifecycleSys.Set(bucket, bucketLifecycle)
|
||||
globalNotificationSys.SetBucketLifecycle(ctx, bucket, bucketLifecycle)
|
||||
|
||||
// Success.
|
||||
|
||||
@@ -35,11 +35,11 @@ const (
|
||||
// LifecycleSys - Bucket lifecycle subsystem.
|
||||
type LifecycleSys struct {
|
||||
sync.RWMutex
|
||||
bucketLifecycleMap map[string]lifecycle.Lifecycle
|
||||
bucketLifecycleMap map[string]*lifecycle.Lifecycle
|
||||
}
|
||||
|
||||
// Set - sets lifecycle config to given bucket name.
|
||||
func (sys *LifecycleSys) Set(bucketName string, lifecycle lifecycle.Lifecycle) {
|
||||
func (sys *LifecycleSys) Set(bucketName string, lifecycle *lifecycle.Lifecycle) {
|
||||
if globalIsGateway {
|
||||
// no-op
|
||||
return
|
||||
@@ -52,7 +52,7 @@ func (sys *LifecycleSys) Set(bucketName string, lifecycle lifecycle.Lifecycle) {
|
||||
}
|
||||
|
||||
// Get - gets lifecycle config associated to a given bucket name.
|
||||
func (sys *LifecycleSys) Get(bucketName string) (lc lifecycle.Lifecycle, ok bool) {
|
||||
func (sys *LifecycleSys) Get(bucketName string) (lc *lifecycle.Lifecycle, ok bool) {
|
||||
if globalIsGateway {
|
||||
// When gateway is enabled, no cached value
|
||||
// is used to validate life cycle policies.
|
||||
@@ -65,7 +65,7 @@ func (sys *LifecycleSys) Get(bucketName string) (lc lifecycle.Lifecycle, ok bool
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return *l, true
|
||||
return l, true
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
@@ -104,8 +104,8 @@ func removeLifecycleConfig(ctx context.Context, objAPI ObjectLayer, bucketName s
|
||||
// Construct path to lifecycle.xml for the given bucket.
|
||||
configFile := path.Join(bucketConfigPrefix, bucketName, bucketLifecycleConfig)
|
||||
|
||||
if err := objAPI.DeleteObject(ctx, minioMetaBucket, configFile); err != nil {
|
||||
if _, ok := err.(ObjectNotFound); ok {
|
||||
if err := deleteConfig(ctx, objAPI, configFile); err != nil {
|
||||
if err == errConfigNotFound {
|
||||
return BucketLifecycleNotFound{Bucket: bucketName}
|
||||
}
|
||||
return err
|
||||
@@ -116,7 +116,7 @@ func removeLifecycleConfig(ctx context.Context, objAPI ObjectLayer, bucketName s
|
||||
// NewLifecycleSys - creates new lifecycle system.
|
||||
func NewLifecycleSys() *LifecycleSys {
|
||||
return &LifecycleSys{
|
||||
bucketLifecycleMap: make(map[string]lifecycle.Lifecycle),
|
||||
bucketLifecycleMap: make(map[string]*lifecycle.Lifecycle),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,15 +139,16 @@ func (sys *LifecycleSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
// Loads lifecycle policies for all buckets into LifecycleSys.
|
||||
func (sys *LifecycleSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
config, err := objAPI.GetBucketLifecycle(GlobalContext, bucket.Name)
|
||||
config, err := getLifecycleConfig(objAPI, bucket.Name)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketLifecycleNotFound); ok {
|
||||
sys.Remove(bucket.Name)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
// Quorum errors should be returned.
|
||||
return err
|
||||
}
|
||||
|
||||
sys.Set(bucket.Name, *config)
|
||||
sys.Set(bucket.Name, config)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyBucketObjectLockEnabledConfigFile = "object-lock-enabled.json"
|
||||
legacyBucketObjectLockEnabledConfig = `{"x-amz-bucket-object-lock-enabled":true}`
|
||||
|
||||
bucketMetadataFile = ".metadata.bin"
|
||||
bucketMetadataFormat = 1
|
||||
bucketMetadataVersion = 1
|
||||
)
|
||||
|
||||
//go:generate msgp -file $GOFILE -unexported
|
||||
|
||||
// bucketMetadata contains bucket metadata.
|
||||
// When adding/removing fields, regenerate the marshal code using the go generate above.
|
||||
// Only changing meaning of fields requires a version bump.
|
||||
// bucketMetadataFormat refers to the format.
|
||||
// bucketMetadataVersion can be used to track a rolling upgrade of a field.
|
||||
type bucketMetadata struct {
|
||||
Name string
|
||||
Created time.Time
|
||||
LockEnabled bool
|
||||
}
|
||||
|
||||
// newBucketMetadata creates bucketMetadata with the supplied name and Created to Now.
|
||||
func newBucketMetadata(name string) bucketMetadata {
|
||||
return bucketMetadata{
|
||||
Name: name,
|
||||
Created: UTCNow(),
|
||||
}
|
||||
}
|
||||
|
||||
// loadBucketMeta loads the metadata of bucket by name from ObjectLayer o.
|
||||
// If an error is returned the returned metadata will be default initialized.
|
||||
func loadBucketMeta(ctx context.Context, o ObjectLayer, name string) (bucketMetadata, error) {
|
||||
b := newBucketMetadata(name)
|
||||
configFile := path.Join(bucketConfigPrefix, name, bucketMetadataFile)
|
||||
data, err := readConfig(ctx, o, configFile)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
if len(data) <= 4 {
|
||||
return b, fmt.Errorf("loadBucketMetadata: no data")
|
||||
}
|
||||
// Read header
|
||||
switch binary.LittleEndian.Uint16(data[0:2]) {
|
||||
case bucketMetadataFormat:
|
||||
default:
|
||||
return b, fmt.Errorf("loadBucketMetadata: unknown format: %d", binary.LittleEndian.Uint16(data[0:2]))
|
||||
}
|
||||
switch binary.LittleEndian.Uint16(data[2:4]) {
|
||||
case bucketMetadataVersion:
|
||||
default:
|
||||
return b, fmt.Errorf("loadBucketMetadata: unknown version: %d", binary.LittleEndian.Uint16(data[2:4]))
|
||||
}
|
||||
|
||||
// OK, parse data.
|
||||
_, err = b.UnmarshalMsg(data[4:])
|
||||
return b, err
|
||||
}
|
||||
|
||||
var errMetaDataConverted = errors.New("metadata converted")
|
||||
|
||||
// loadBucketMetadata loads and migrates to bucket metadata.
|
||||
func loadBucketMetadata(ctx context.Context, objectAPI ObjectLayer, bucket string) (bucketMetadata, error) {
|
||||
meta, err := loadBucketMeta(ctx, objectAPI, bucket)
|
||||
if err == nil {
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
if err != errConfigNotFound {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
// Control here means old bucket without bucket metadata. Hence we migrate existing settings.
|
||||
if err = meta.convertLegacyLockconfig(ctx, objectAPI); err != nil {
|
||||
return meta, err
|
||||
}
|
||||
|
||||
return meta, errMetaDataConverted
|
||||
}
|
||||
|
||||
func (b *bucketMetadata) convertLegacyLockconfig(ctx context.Context, objectAPI ObjectLayer) error {
|
||||
configFile := path.Join(bucketConfigPrefix, b.Name, legacyBucketObjectLockEnabledConfigFile)
|
||||
|
||||
save := func() error {
|
||||
if err := b.save(ctx, objectAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteConfig(ctx, objectAPI, configFile); err != nil && !errors.Is(err, errConfigNotFound) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
configData, err := readConfig(ctx, objectAPI, configFile)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errConfigNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
return save()
|
||||
}
|
||||
|
||||
if string(configData) != legacyBucketObjectLockEnabledConfig {
|
||||
return fmt.Errorf("content mismatch in config file %v", configFile)
|
||||
}
|
||||
|
||||
b.LockEnabled = true
|
||||
return save()
|
||||
}
|
||||
|
||||
// save config to supplied ObjectLayer o.
|
||||
func (b *bucketMetadata) save(ctx context.Context, o ObjectLayer) error {
|
||||
data := make([]byte, 4, b.Msgsize()+4)
|
||||
// Put header
|
||||
binary.LittleEndian.PutUint16(data[0:2], bucketMetadataFormat)
|
||||
binary.LittleEndian.PutUint16(data[2:4], bucketMetadataVersion)
|
||||
|
||||
// Add data
|
||||
data, err := b.MarshalMsg(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
|
||||
return saveConfig(ctx, o, configFile, data)
|
||||
}
|
||||
|
||||
// delete the config metadata.
|
||||
// If config does not exist no error is returned.
|
||||
func (b bucketMetadata) delete(ctx context.Context, o ObjectLayer) error {
|
||||
configFile := path.Join(bucketConfigPrefix, b.Name, bucketMetadataFile)
|
||||
err := deleteConfig(ctx, o, configFile)
|
||||
if err == errConfigNotFound {
|
||||
// We don't care
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package cmd
|
||||
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *bucketMetadata) 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 "Created":
|
||||
z.Created, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
case "LockEnabled":
|
||||
z.LockEnabled, err = dc.ReadBool()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockEnabled")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z bucketMetadata) 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 "Created"
|
||||
err = en.Append(0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z.Created)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
// write "LockEnabled"
|
||||
err = en.Append(0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteBool(z.LockEnabled)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockEnabled")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z bucketMetadata) 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 "Created"
|
||||
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||
o = msgp.AppendTime(o, z.Created)
|
||||
// string "LockEnabled"
|
||||
o = append(o, 0xab, 0x4c, 0x6f, 0x63, 0x6b, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64)
|
||||
o = msgp.AppendBool(o, z.LockEnabled)
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *bucketMetadata) 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 "Created":
|
||||
z.Created, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "Created")
|
||||
return
|
||||
}
|
||||
case "LockEnabled":
|
||||
z.LockEnabled, bts, err = msgp.ReadBoolBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "LockEnabled")
|
||||
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 bucketMetadata) Msgsize() (s int) {
|
||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package cmd
|
||||
|
||||
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tinylib/msgp/msgp"
|
||||
)
|
||||
|
||||
func TestMarshalUnmarshalbucketMetadata(t *testing.T) {
|
||||
v := bucketMetadata{}
|
||||
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 BenchmarkMarshalMsgbucketMetadata(b *testing.B) {
|
||||
v := bucketMetadata{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgbucketMetadata(b *testing.B) {
|
||||
v := bucketMetadata{}
|
||||
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 BenchmarkUnmarshalbucketMetadata(b *testing.B) {
|
||||
v := bucketMetadata{}
|
||||
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 TestEncodeDecodebucketMetadata(t *testing.T) {
|
||||
v := bucketMetadata{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodebucketMetadata Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := bucketMetadata{}
|
||||
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 BenchmarkEncodebucketMetadata(b *testing.B) {
|
||||
v := bucketMetadata{}
|
||||
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 BenchmarkDecodebucketMetadata(b *testing.B) {
|
||||
v := bucketMetadata{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"time"
|
||||
@@ -296,11 +295,7 @@ func (api objectAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWrit
|
||||
if ev.S3.Bucket.Name != values.Get(peerRESTListenBucket) {
|
||||
return false
|
||||
}
|
||||
objectName, uerr := url.QueryUnescape(ev.S3.Object.Key)
|
||||
if uerr != nil {
|
||||
objectName = ev.S3.Object.Key
|
||||
}
|
||||
return len(rulesMap.Match(ev.EventName, objectName).ToSlice()) != 0
|
||||
return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
|
||||
})
|
||||
|
||||
for _, peer := range peers {
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
// Similar to enforceRetentionBypassForDelete but for WebUI
|
||||
func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn) APIErrorCode {
|
||||
func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, govBypassPerms bool) APIErrorCode {
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
return toAPIErrorCode(ctx, err)
|
||||
@@ -80,7 +80,7 @@ func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bu
|
||||
// and must explicitly include x-amz-bypass-governance-retention:true
|
||||
// as a request header with any request that requires overriding
|
||||
// governance mode.
|
||||
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||
byPassSet := govBypassPerms && objectlock.IsObjectLockGovernanceBypassSet(r.Header)
|
||||
if !byPassSet {
|
||||
t, err := objectlock.UTCNowNTP()
|
||||
if err != nil {
|
||||
@@ -91,6 +91,11 @@ func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bu
|
||||
if !ret.RetainUntilDate.Before(t) {
|
||||
return ErrObjectLocked
|
||||
}
|
||||
|
||||
if !govBypassPerms {
|
||||
return ErrObjectLocked
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
}
|
||||
@@ -98,9 +103,9 @@ func enforceRetentionBypassForDeleteWeb(ctx context.Context, r *http.Request, bu
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// enforceRetentionForLifecycle checks if it is appropriate to remove an
|
||||
// object according to locking configuration when this is lifecycle asking.
|
||||
func enforceRetentionForLifecycle(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
||||
// enforceRetentionForDeletion checks if it is appropriate to remove an
|
||||
// object according to locking configuration when this is lifecycle/ bucket quota asking.
|
||||
func enforceRetentionForDeletion(ctx context.Context, objInfo ObjectInfo) (locked bool) {
|
||||
lhold := objectlock.GetObjectLegalHoldMeta(objInfo.UserDefined)
|
||||
if lhold.Status.Valid() && lhold.Status == objectlock.LegalHoldOn {
|
||||
return true
|
||||
@@ -314,7 +319,7 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
|
||||
if objInfo, err := getObjectInfoFn(ctx, bucket, object, opts); err == nil {
|
||||
objExists = true
|
||||
r := objectlock.GetObjectRetentionMeta(objInfo.UserDefined)
|
||||
if globalWORMEnabled || ((r.Mode == objectlock.RetCompliance) && r.RetainUntilDate.After(t)) {
|
||||
if r.Mode == objectlock.RetCompliance && r.RetainUntilDate.After(t) {
|
||||
return mode, retainDate, legalHold, ErrObjectLocked
|
||||
}
|
||||
mode = r.Mode
|
||||
@@ -376,26 +381,24 @@ func checkPutObjectLockAllowed(ctx context.Context, r *http.Request, bucket, obj
|
||||
func initBucketObjectLockConfig(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
ctx := logger.SetReqInfo(GlobalContext, &logger.ReqInfo{BucketName: bucket.Name})
|
||||
configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketObjectLockEnabledConfigFile)
|
||||
bucketObjLockData, err := readConfig(ctx, objAPI, configFile)
|
||||
meta, err := loadBucketMetadata(ctx, objAPI, bucket.Name)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
continue
|
||||
if err != errMetaDataConverted {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if string(bucketObjLockData) != bucketObjectLockEnabledConfig {
|
||||
// this should never happen
|
||||
logger.LogIf(ctx, objectlock.ErrMalformedBucketObjectConfig)
|
||||
meta.Created = bucket.Created
|
||||
logger.LogIf(ctx, meta.save(ctx, objAPI))
|
||||
}
|
||||
if !meta.LockEnabled {
|
||||
continue
|
||||
}
|
||||
|
||||
configFile = path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
|
||||
configFile := path.Join(bucketConfigPrefix, bucket.Name, objectLockConfig)
|
||||
configData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
globalBucketObjectLockConfig.Set(bucket.Name, objectlock.Retention{})
|
||||
globalBucketObjectLockConfig.Set(bucket.Name, &objectlock.Retention{})
|
||||
continue
|
||||
}
|
||||
return err
|
||||
@@ -405,7 +408,7 @@ func initBucketObjectLockConfig(buckets []BucketInfo, objAPI ObjectLayer) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
retention := objectlock.Retention{}
|
||||
retention := &objectlock.Retention{}
|
||||
if config.Rule != nil {
|
||||
retention = config.ToRetention()
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func (api objectAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
|
||||
globalPolicySys.Set(bucket, *bucketPolicy)
|
||||
globalPolicySys.Set(bucket, bucketPolicy)
|
||||
globalNotificationSys.SetBucketPolicy(ctx, bucket, bucketPolicy)
|
||||
|
||||
// Success.
|
||||
|
||||
@@ -39,11 +39,11 @@ import (
|
||||
// PolicySys - policy subsystem.
|
||||
type PolicySys struct {
|
||||
sync.RWMutex
|
||||
bucketPolicyMap map[string]policy.Policy
|
||||
bucketPolicyMap map[string]*policy.Policy
|
||||
}
|
||||
|
||||
// Set - sets policy to given bucket name. If policy is empty, existing policy is removed.
|
||||
func (sys *PolicySys) Set(bucketName string, policy policy.Policy) {
|
||||
func (sys *PolicySys) Set(bucketName string, policy *policy.Policy) {
|
||||
if globalIsGateway {
|
||||
// Set policy is a non-op under gateway mode.
|
||||
return
|
||||
@@ -97,12 +97,12 @@ func (sys *PolicySys) IsAllowed(args policy.Args) bool {
|
||||
// Loads policies for all buckets into PolicySys.
|
||||
func (sys *PolicySys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
config, err := objAPI.GetBucketPolicy(GlobalContext, bucket.Name)
|
||||
config, err := getPolicyConfig(objAPI, bucket.Name)
|
||||
if err != nil {
|
||||
if _, ok := err.(BucketPolicyNotFound); ok {
|
||||
sys.Remove(bucket.Name)
|
||||
continue
|
||||
}
|
||||
continue
|
||||
return err
|
||||
}
|
||||
// This part is specifically written to handle migration
|
||||
// when the Version string is empty, this was allowed
|
||||
@@ -118,7 +118,7 @@ func (sys *PolicySys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sys.Set(bucket.Name, *config)
|
||||
sys.Set(bucket.Name, config)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func (sys *PolicySys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
// Init - initializes policy system from policy.json of all buckets.
|
||||
func (sys *PolicySys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// In gateway mode, we don't need to load the policies
|
||||
@@ -142,7 +142,7 @@ func (sys *PolicySys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
// NewPolicySys - creates new policy system.
|
||||
func NewPolicySys() *PolicySys {
|
||||
return &PolicySys{
|
||||
bucketPolicyMap: make(map[string]policy.Policy),
|
||||
bucketPolicyMap: make(map[string]*policy.Policy),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/config"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
|
||||
// BucketQuotaSys - map of bucket and quota configuration.
|
||||
type BucketQuotaSys struct {
|
||||
sync.RWMutex
|
||||
quotaMap map[string]madmin.BucketQuota
|
||||
}
|
||||
|
||||
// Set - set quota configuration.
|
||||
func (sys *BucketQuotaSys) Set(bucketName string, q madmin.BucketQuota) {
|
||||
sys.Lock()
|
||||
sys.quotaMap[bucketName] = q
|
||||
sys.Unlock()
|
||||
}
|
||||
|
||||
// Get - Get quota configuration.
|
||||
func (sys *BucketQuotaSys) Get(bucketName string) (q madmin.BucketQuota, ok bool) {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
q, ok = sys.quotaMap[bucketName]
|
||||
return
|
||||
}
|
||||
|
||||
// Remove - removes quota configuration.
|
||||
func (sys *BucketQuotaSys) Remove(bucketName string) {
|
||||
sys.Lock()
|
||||
delete(sys.quotaMap, bucketName)
|
||||
sys.Unlock()
|
||||
}
|
||||
|
||||
// Buckets - list of buckets with quota configuration
|
||||
func (sys *BucketQuotaSys) Buckets() []string {
|
||||
sys.RLock()
|
||||
defer sys.RUnlock()
|
||||
var buckets []string
|
||||
for k := range sys.quotaMap {
|
||||
buckets = append(buckets, k)
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
// Init initialize bucket quota sys configuration with all buckets.
|
||||
func (sys *BucketQuotaSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// In gateway mode, we do not support bucket quota.
|
||||
// So, this is a no-op for gateway servers.
|
||||
if globalIsGateway {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sys.load(buckets, objAPI)
|
||||
}
|
||||
|
||||
func (sys *BucketQuotaSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
ctx := logger.SetReqInfo(GlobalContext, &logger.ReqInfo{BucketName: bucket.Name})
|
||||
configFile := path.Join(bucketConfigPrefix, bucket.Name, bucketQuotaConfigFile)
|
||||
configData, err := readConfig(ctx, objAPI, configFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
quotaCfg, err := parseBucketQuota(configData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
globalBucketQuotaSys.Set(bucket.Name, quotaCfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBucketQuotaSys returns initialized BucketQuotaSys
|
||||
func NewBucketQuotaSys() *BucketQuotaSys {
|
||||
return &BucketQuotaSys{quotaMap: map[string]madmin.BucketQuota{}}
|
||||
}
|
||||
|
||||
// parseBucketQuota parses BucketQuota from json
|
||||
func parseBucketQuota(data []byte) (quotaCfg madmin.BucketQuota, err error) {
|
||||
err = json.Unmarshal(data, "aCfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !quotaCfg.Type.IsValid() {
|
||||
return quotaCfg, fmt.Errorf("Invalid quota type %s", quotaCfg.Type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type bucketStorageCache struct {
|
||||
bucketsSizes map[string]uint64
|
||||
lastUpdate time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (b *bucketStorageCache) check(ctx context.Context, q madmin.BucketQuota, bucket string, size int64) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if time.Since(b.lastUpdate) > 10*time.Second {
|
||||
dui, err := loadDataUsageFromBackend(ctx, newObjectLayerWithoutSafeModeFn())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.lastUpdate = time.Now()
|
||||
b.bucketsSizes = dui.BucketsSizes
|
||||
}
|
||||
currUsage := b.bucketsSizes[bucket]
|
||||
if (currUsage + uint64(size)) > q.Quota {
|
||||
return BucketQuotaExceeded{Bucket: bucket}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func enforceBucketQuota(ctx context.Context, bucket string, size int64) error {
|
||||
if size < 0 {
|
||||
return nil
|
||||
}
|
||||
q, ok := globalBucketQuotaSys.Get(bucket)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return globalBucketStorageCache.check(ctx, q, bucket, size)
|
||||
}
|
||||
|
||||
const (
|
||||
bgQuotaInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
// initQuotaEnforcement starts the routine that deletes objects in bucket
|
||||
// that exceeds the FIFO quota
|
||||
func initQuotaEnforcement(ctx context.Context, objAPI ObjectLayer) {
|
||||
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn {
|
||||
go startBucketQuotaEnforcement(ctx, objAPI)
|
||||
}
|
||||
}
|
||||
|
||||
func startBucketQuotaEnforcement(ctx context.Context, objAPI ObjectLayer) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.NewTimer(bgQuotaInterval).C:
|
||||
logger.LogIf(ctx, enforceFIFOQuota(ctx, objAPI))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// enforceFIFOQuota deletes objects in FIFO order until sufficient objects
|
||||
// have been deleted so as to bring bucket usage within quota
|
||||
func enforceFIFOQuota(ctx context.Context, objectAPI ObjectLayer) error {
|
||||
// Turn off quota enforcement if data usage info is unavailable.
|
||||
if env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOff {
|
||||
return nil
|
||||
}
|
||||
for _, bucket := range globalBucketQuotaSys.Buckets() {
|
||||
// Check if the current bucket has quota restrictions, if not skip it
|
||||
cfg, ok := globalBucketQuotaSys.Get(bucket)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if cfg.Type != madmin.FIFOQuota {
|
||||
continue
|
||||
}
|
||||
_, bucketHasLockConfig := globalBucketObjectLockConfig.Get(bucket)
|
||||
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var toFree uint64
|
||||
if dataUsageInfo.BucketsSizes[bucket] > cfg.Quota {
|
||||
toFree = dataUsageInfo.BucketsSizes[bucket] - cfg.Quota
|
||||
}
|
||||
if toFree <= 0 {
|
||||
continue
|
||||
}
|
||||
// Allocate new results channel to receive ObjectInfo.
|
||||
objInfoCh := make(chan ObjectInfo)
|
||||
|
||||
// Walk through all objects
|
||||
if err := objectAPI.Walk(ctx, bucket, "", objInfoCh); err != nil {
|
||||
return err
|
||||
}
|
||||
// reuse the fileScorer used by disk cache to score entries by
|
||||
// ModTime to find the oldest objects in bucket to delete. In
|
||||
// the context of bucket quota enforcement - number of hits are
|
||||
// irrelevant.
|
||||
scorer, err := newFileScorer(toFree, time.Now().Unix(), 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for obj := range objInfoCh {
|
||||
// skip objects currently under retention
|
||||
if bucketHasLockConfig && enforceRetentionForDeletion(ctx, obj) {
|
||||
continue
|
||||
}
|
||||
scorer.addFile(obj.Name, obj.ModTime, obj.Size, 1)
|
||||
}
|
||||
var objects []string
|
||||
numKeys := len(scorer.fileNames())
|
||||
for i, key := range scorer.fileNames() {
|
||||
objects = append(objects, key)
|
||||
if len(objects) < maxObjectList && (i < numKeys-1) {
|
||||
// skip deletion until maxObjectList or end of slice
|
||||
continue
|
||||
}
|
||||
|
||||
if len(objects) == 0 {
|
||||
break
|
||||
}
|
||||
// Deletes a list of objects.
|
||||
deleteErrs, err := objectAPI.DeleteObjects(ctx, bucket, objects)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
} else {
|
||||
for i := range deleteErrs {
|
||||
if deleteErrs[i] != nil {
|
||||
logger.LogIf(ctx, deleteErrs[i])
|
||||
continue
|
||||
}
|
||||
// Notify object deleted event.
|
||||
sendEvent(eventArgs{
|
||||
EventName: event.ObjectRemovedDelete,
|
||||
BucketName: bucket,
|
||||
Object: ObjectInfo{
|
||||
Name: objects[i],
|
||||
},
|
||||
Host: "Internal: [FIFO-QUOTA-EXPIRY]",
|
||||
})
|
||||
}
|
||||
objects = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+8
-7
@@ -173,7 +173,14 @@ func handleCommonCmdArgs(ctx *cli.Context) {
|
||||
}
|
||||
|
||||
func handleCommonEnvVars() {
|
||||
var err error
|
||||
wormEnabled, err := config.LookupWorm()
|
||||
if err != nil {
|
||||
logger.Fatal(config.ErrInvalidWormValue(err), "Invalid worm configuration")
|
||||
}
|
||||
if wormEnabled {
|
||||
logger.Fatal(errors.New("WORM is deprecated"), "global MINIO_WORM support is removed, please downgrade your server or migrate to https://github.com/minio/minio/tree/master/docs/retention")
|
||||
}
|
||||
|
||||
globalBrowserEnabled, err = config.ParseBool(env.Get(config.EnvBrowser, config.EnableOn))
|
||||
if err != nil {
|
||||
logger.Fatal(config.ErrInvalidBrowserValue(err), "Invalid MINIO_BROWSER value in environment variable")
|
||||
@@ -239,12 +246,6 @@ func handleCommonEnvVars() {
|
||||
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) {
|
||||
|
||||
+13
-66
@@ -21,6 +21,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3"
|
||||
@@ -35,42 +36,10 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If its server mode or nas gateway, migrate the backend.
|
||||
doneCh := make(chan struct{})
|
||||
|
||||
var encrypted bool
|
||||
var err error
|
||||
|
||||
// Migrating Config backend needs a retry mechanism for
|
||||
// the following reasons:
|
||||
// - Read quorum is lost just after the initialization
|
||||
// of the object layer.
|
||||
retryTimerCh := newRetryTimerSimple(doneCh)
|
||||
var stop bool
|
||||
|
||||
rquorum := InsufficientReadQuorum{}
|
||||
wquorum := InsufficientWriteQuorum{}
|
||||
|
||||
for !stop {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
if encrypted, err = checkBackendEncrypted(objAPI); err != nil {
|
||||
if errors.Is(err, errDiskNotFound) ||
|
||||
errors.As(err, &rquorum) ||
|
||||
isErrBucketNotFound(err) {
|
||||
logger.Info("Waiting for config backend to be encrypted..")
|
||||
continue
|
||||
}
|
||||
close(doneCh)
|
||||
return err
|
||||
}
|
||||
stop = true
|
||||
case <-globalOSSignalCh:
|
||||
close(doneCh)
|
||||
return fmt.Errorf("Config encryption process stopped gracefully")
|
||||
}
|
||||
encrypted, err := checkBackendEncrypted(objAPI)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to encrypt config %w", err)
|
||||
}
|
||||
close(doneCh)
|
||||
|
||||
if encrypted {
|
||||
// backend is encrypted, but credentials are not specified
|
||||
@@ -90,34 +59,12 @@ func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
doneCh = make(chan struct{})
|
||||
defer close(doneCh)
|
||||
|
||||
retryTimerCh = newRetryTimerSimple(doneCh)
|
||||
|
||||
// Migrating Config backend needs a retry mechanism for
|
||||
// the following reasons:
|
||||
// - Read quorum is lost just after the initialization
|
||||
// of the object layer.
|
||||
for {
|
||||
select {
|
||||
case <-retryTimerCh:
|
||||
// Migrate IAM configuration
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI, globalOldCred, encrypted); err != nil {
|
||||
if errors.Is(err, errDiskNotFound) ||
|
||||
errors.As(err, &rquorum) ||
|
||||
errors.As(err, &wquorum) ||
|
||||
isErrBucketNotFound(err) {
|
||||
logger.Info("Waiting for config backend to be encrypted..")
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-globalOSSignalCh:
|
||||
return fmt.Errorf("Config encryption process stopped gracefully")
|
||||
}
|
||||
// Migrate IAM configuration
|
||||
if err = migrateConfigPrefixToEncrypted(objAPI, globalOldCred, encrypted); err != nil {
|
||||
return fmt.Errorf("Unable to migrate all config at .minio.sys/config/: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -163,9 +110,6 @@ func decryptData(edata []byte, creds ...auth.Credentials) ([]byte, error) {
|
||||
}
|
||||
|
||||
func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, defaultContextTimeout)
|
||||
defer cancel()
|
||||
|
||||
encrypted, err := checkBackendEtcdEncrypted(ctx, client)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -206,7 +150,10 @@ func migrateIAMConfigsEtcdToEncrypted(ctx context.Context, client *etcd.Client)
|
||||
logger.Info("Attempting encryption of all IAM users and policies on etcd")
|
||||
}
|
||||
|
||||
r, err := client.Get(ctx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
listCtx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
r, err := client.Get(listCtx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 config
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
color "github.com/minio/minio/pkg/color"
|
||||
)
|
||||
|
||||
// Extra ASN1 OIDs that we may need to handle
|
||||
var (
|
||||
oidEmailAddress = []int{1, 2, 840, 113549, 1, 9, 1}
|
||||
)
|
||||
|
||||
// printName prints the fields of a distinguished name, which include such
|
||||
// things as its common name and locality.
|
||||
func printName(names []pkix.AttributeTypeAndValue, buf *strings.Builder) []string {
|
||||
values := []string{}
|
||||
for _, name := range names {
|
||||
oid := name.Type
|
||||
if len(oid) == 4 && oid[0] == 2 && oid[1] == 5 && oid[2] == 4 {
|
||||
switch oid[3] {
|
||||
case 3:
|
||||
values = append(values, fmt.Sprintf("CN=%s", name.Value))
|
||||
case 6:
|
||||
values = append(values, fmt.Sprintf("C=%s", name.Value))
|
||||
case 8:
|
||||
values = append(values, fmt.Sprintf("ST=%s", name.Value))
|
||||
case 10:
|
||||
values = append(values, fmt.Sprintf("O=%s", name.Value))
|
||||
case 11:
|
||||
values = append(values, fmt.Sprintf("OU=%s", name.Value))
|
||||
default:
|
||||
values = append(values, fmt.Sprintf("UnknownOID=%s", name.Type.String()))
|
||||
}
|
||||
} else if oid.Equal(oidEmailAddress) {
|
||||
values = append(values, fmt.Sprintf("emailAddress=%s", name.Value))
|
||||
} else {
|
||||
values = append(values, fmt.Sprintf("UnknownOID=%s", name.Type.String()))
|
||||
}
|
||||
}
|
||||
if len(values) > 0 {
|
||||
buf.WriteString(values[0])
|
||||
for i := 1; i < len(values); i++ {
|
||||
buf.WriteString(", " + values[i])
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// CertificateText returns a human-readable string representation
|
||||
// of the certificate cert. The format is similar to the OpenSSL
|
||||
// way of printing certificates (not identical).
|
||||
func CertificateText(cert *x509.Certificate) string {
|
||||
var buf strings.Builder
|
||||
|
||||
buf.WriteString(color.Blue("\nCertificate:\n"))
|
||||
if cert.SignatureAlgorithm != x509.UnknownSignatureAlgorithm {
|
||||
buf.WriteString(color.Blue("%4sSignature Algorithm: ", "") + color.Bold(fmt.Sprintf("%s\n", cert.SignatureAlgorithm)))
|
||||
}
|
||||
|
||||
// Issuer information
|
||||
buf.WriteString(color.Blue("%4sIssuer: ", ""))
|
||||
printName(cert.Issuer.Names, &buf)
|
||||
|
||||
// Validity information
|
||||
buf.WriteString(color.Blue("%4sValidity\n", ""))
|
||||
buf.WriteString(color.Bold(fmt.Sprintf("%8sNot Before: %s\n", "", cert.NotBefore.Format(http.TimeFormat))))
|
||||
buf.WriteString(color.Bold(fmt.Sprintf("%8sNot After : %s\n", "", cert.NotAfter.Format(http.TimeFormat))))
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
@@ -169,6 +169,12 @@ Example 1:
|
||||
$ minio server /data/minio/`,
|
||||
)
|
||||
|
||||
ErrUnsupportedBackend = newErrFn(
|
||||
"Unable to write to the backend",
|
||||
"Please ensure your disk supports O_DIRECT",
|
||||
"",
|
||||
)
|
||||
|
||||
ErrUnableToWriteInBackend = newErrFn(
|
||||
"Unable to write to the backend",
|
||||
"Please ensure MinIO binary has write permissions for the backend",
|
||||
|
||||
@@ -58,6 +58,7 @@ type Config struct {
|
||||
stsExpiryDuration time.Duration // contains converted value
|
||||
tlsSkipVerify bool // allows skipping TLS verification
|
||||
serverInsecure bool // allows plain text connection to LDAP Server
|
||||
serverStartTLS bool // allows plain text connection to LDAP Server
|
||||
rootCAs *x509.CertPool
|
||||
}
|
||||
|
||||
@@ -73,11 +74,13 @@ const (
|
||||
GroupSearchBaseDN = "group_search_base_dn"
|
||||
TLSSkipVerify = "tls_skip_verify"
|
||||
ServerInsecure = "server_insecure"
|
||||
ServerStartTLS = "server_starttls"
|
||||
|
||||
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"
|
||||
EnvServerStartTLS = "MINIO_IDENTITY_LDAP_SERVER_STARTTLS"
|
||||
EnvUsernameFormat = "MINIO_IDENTITY_LDAP_USERNAME_FORMAT"
|
||||
EnvUsernameSearchFilter = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_FILTER"
|
||||
EnvUsernameSearchBaseDN = "MINIO_IDENTITY_LDAP_USERNAME_SEARCH_BASE_DN"
|
||||
@@ -129,6 +132,10 @@ var (
|
||||
Key: ServerInsecure,
|
||||
Value: config.EnableOff,
|
||||
},
|
||||
config.KV{
|
||||
Key: ServerStartTLS,
|
||||
Value: config.EnableOff,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -257,6 +264,18 @@ func (l *Config) Connect() (ldapConn *ldap.Conn, err error) {
|
||||
return ldap.Dial("tcp", l.ServerAddr)
|
||||
}
|
||||
|
||||
if l.serverStartTLS {
|
||||
conn, err := ldap.Dial("tcp", l.ServerAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = conn.StartTLS(&tls.Config{
|
||||
InsecureSkipVerify: l.tlsSkipVerify,
|
||||
RootCAs: l.rootCAs,
|
||||
})
|
||||
return conn, err
|
||||
}
|
||||
|
||||
return ldap.DialTLS("tcp", l.ServerAddr, &tls.Config{
|
||||
InsecureSkipVerify: l.tlsSkipVerify,
|
||||
RootCAs: l.rootCAs,
|
||||
@@ -303,6 +322,12 @@ func Lookup(kvs config.KVS, rootCAs *x509.CertPool) (l Config, err error) {
|
||||
return l, err
|
||||
}
|
||||
}
|
||||
if v := env.Get(EnvServerStartTLS, kvs.Get(ServerStartTLS)); v != "" {
|
||||
l.serverStartTLS, 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 {
|
||||
|
||||
+73
-41
@@ -1,4 +1,4 @@
|
||||
// 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.
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -27,6 +28,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
)
|
||||
|
||||
// KesConfig contains the configuration required
|
||||
@@ -112,6 +117,7 @@ func NewKes(cfg KesConfig) (KMS, error) {
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: certPool,
|
||||
}
|
||||
cfg.Transport.ForceAttemptHTTP2 = true
|
||||
return &kesService{
|
||||
client: &kesClient{
|
||||
addr: cfg.Endpoint,
|
||||
@@ -208,6 +214,18 @@ type kesClient struct {
|
||||
httpClient http.Client
|
||||
}
|
||||
|
||||
// Response KES response struct
|
||||
type response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
Ciphertext []byte `json:"ciphertext,omitempty"`
|
||||
}
|
||||
|
||||
// Request KES request struct
|
||||
type request struct {
|
||||
Ciphertext []byte `json:"ciphertext,omitempty"`
|
||||
Context []byte `json:"context"`
|
||||
}
|
||||
|
||||
// GenerateDataKey requests a new data key from the KES server.
|
||||
// On success, the KES server will respond with the plaintext key
|
||||
// and the ciphertext key as the plaintext key encrypted with
|
||||
@@ -217,10 +235,7 @@ type kesClient struct {
|
||||
// such that you have to provide the same context when decrypting
|
||||
// the data key.
|
||||
func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte, error) {
|
||||
type Request struct {
|
||||
Context []byte `json:"context"`
|
||||
}
|
||||
body, err := json.Marshal(Request{
|
||||
body, err := json.Marshal(request{
|
||||
Context: context,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -228,25 +243,57 @@ func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/key/generate/%s", c.addr, url.PathEscape(name))
|
||||
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(body))
|
||||
|
||||
const limit = 1 << 20 // A plaintext/ciphertext key pair will never be larger than 1 MB
|
||||
resp, err := c.postRetry(url, bytes.NewReader(body), limit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, c.parseErrorResponse(resp)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
type Response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
return resp.Plaintext, resp.Ciphertext, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) post(url string, body io.Reader, limit int64) (*response, error) {
|
||||
resp, err := c.httpClient.Post(url, "application/json", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const limit = 1 << 20 // A plaintext/ciphertext key pair will never be larger than 1 MB
|
||||
var response Response
|
||||
if err = json.NewDecoder(io.LimitReader(resp.Body, limit)).Decode(&response); err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
// Drain the entire body to make sure we have re-use connections
|
||||
defer xhttp.DrainBody(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, c.parseErrorResponse(resp)
|
||||
}
|
||||
|
||||
response := &response{}
|
||||
if err = json.NewDecoder(io.LimitReader(resp.Body, limit)).Decode(response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) postRetry(url string, body io.ReadSeeker, limit int64) (*response, error) {
|
||||
for i := 0; ; i++ {
|
||||
body.Seek(0, io.SeekStart) // seek to the beginning of the body.
|
||||
|
||||
response, err := c.post(url, body, limit)
|
||||
if err == nil {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
if !xnet.IsNetworkOrHostDown(err) && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// retriable network errors.
|
||||
remain := retryMax - i
|
||||
if remain <= 0 {
|
||||
return response, err
|
||||
}
|
||||
|
||||
<-time.After(LinearJitterBackoff(retryWaitMin, retryWaitMax, i))
|
||||
}
|
||||
return response.Plaintext, response.Ciphertext, nil
|
||||
}
|
||||
|
||||
// GenerateDataKey decrypts an encrypted data key with the key
|
||||
@@ -256,11 +303,7 @@ func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte
|
||||
// The optional context must match the value you provided when
|
||||
// generating the data key.
|
||||
func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) {
|
||||
type Request struct {
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
Context []byte `json:"context"`
|
||||
}
|
||||
body, err := json.Marshal(Request{
|
||||
body, err := json.Marshal(request{
|
||||
Ciphertext: ciphertext,
|
||||
Context: context,
|
||||
})
|
||||
@@ -269,37 +312,26 @@ func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]b
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/key/decrypt/%s", c.addr, url.PathEscape(name))
|
||||
resp, err := c.httpClient.Post(url, "application/json", bytes.NewReader(body))
|
||||
|
||||
const limit = 32 * 1024 // A data key will never be larger than 32 KB
|
||||
resp, err := c.postRetry(url, bytes.NewReader(body), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, c.parseErrorResponse(resp)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
type Response struct {
|
||||
Plaintext []byte `json:"plaintext"`
|
||||
}
|
||||
const limit = 32 * 1024 // A data key will never be larger than 32 KB
|
||||
var response Response
|
||||
if err = json.NewDecoder(io.LimitReader(resp.Body, limit)).Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.Plaintext, nil
|
||||
return resp.Plaintext, nil
|
||||
}
|
||||
|
||||
func (c *kesClient) parseErrorResponse(resp *http.Response) error {
|
||||
if resp.Body == nil {
|
||||
return nil
|
||||
return Errorf("%s: no body", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
const limit = 32 * 1024 // A (valid) error response will not be greater than 32 KB
|
||||
var errMsg strings.Builder
|
||||
if _, err := io.Copy(&errMsg, io.LimitReader(resp.Body, limit)); err != nil {
|
||||
return err
|
||||
return Errorf("%s: %s", http.StatusText(resp.StatusCode), err)
|
||||
}
|
||||
|
||||
return Errorf("%s: %s", http.StatusText(resp.StatusCode), errMsg.String())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 crypto
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// default retry configuration
|
||||
const (
|
||||
retryWaitMin = 500 * time.Millisecond // minimum retry limit.
|
||||
retryWaitMax = 3 * time.Second // 3 secs worth of max retry.
|
||||
retryMax = 2
|
||||
)
|
||||
|
||||
// LinearJitterBackoff provides the time.Duration for a caller to
|
||||
// perform linear backoff based on the attempt number and with jitter to
|
||||
// prevent a thundering herd.
|
||||
//
|
||||
// min and max here are *not* absolute values. The number to be multiplied by
|
||||
// the attempt number will be chosen at random from between them, thus they are
|
||||
// bounding the jitter.
|
||||
//
|
||||
// For instance:
|
||||
// * To get strictly linear backoff of one second increasing each retry, set
|
||||
// both to one second (1s, 2s, 3s, 4s, ...)
|
||||
// * To get a small amount of jitter centered around one second increasing each
|
||||
// retry, set to around one second, such as a min of 800ms and max of 1200ms
|
||||
// (892ms, 2102ms, 2945ms, 4312ms, ...)
|
||||
// * To get extreme jitter, set to a very wide spread, such as a min of 100ms
|
||||
// and a max of 20s (15382ms, 292ms, 51321ms, 35234ms, ...)
|
||||
func LinearJitterBackoff(min, max time.Duration, attemptNum int) time.Duration {
|
||||
// attemptNum always starts at zero but we want to start at 1 for multiplication
|
||||
attemptNum++
|
||||
|
||||
if max <= min {
|
||||
// Unclear what to do here, or they are the same, so return min *
|
||||
// attemptNum
|
||||
return min * time.Duration(attemptNum)
|
||||
}
|
||||
|
||||
// Seed rand; doing this every time is fine
|
||||
rand := rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
|
||||
|
||||
// Pick a random number that lies somewhere between the min and max and
|
||||
// multiply by the attemptNum. attemptNum starts at zero so we always
|
||||
// increment here. We first get a random percentage, then apply that to the
|
||||
// difference between min and max, and add to min.
|
||||
jitter := rand.Float64() * float64(max-min)
|
||||
jitterMin := int64(jitter) + int64(min)
|
||||
return time.Duration(jitterMin * int64(attemptNum))
|
||||
}
|
||||
@@ -51,11 +51,6 @@ func startDailyLifecycle(ctx context.Context, objAPI ObjectLayer) {
|
||||
}
|
||||
|
||||
func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
// No action is expected when WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
buckets, err := objAPI.ListBuckets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -94,7 +89,7 @@ func lifecycleRound(ctx context.Context, objAPI ObjectLayer) error {
|
||||
}
|
||||
// Find the action that need to be executed
|
||||
if l.ComputeAction(obj.Name, obj.UserTags, obj.ModTime) == lifecycle.DeleteAction {
|
||||
if bucketHasLockConfig && enforceRetentionForLifecycle(ctx, obj) {
|
||||
if bucketHasLockConfig && enforceRetentionForDeletion(ctx, obj) {
|
||||
continue
|
||||
}
|
||||
objects = append(objects, obj.Name)
|
||||
|
||||
@@ -0,0 +1,613 @@
|
||||
/*
|
||||
* 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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/willf/bloom"
|
||||
)
|
||||
|
||||
const (
|
||||
// Estimate bloom filter size. With this many items
|
||||
dataUpdateTrackerEstItems = 1000000
|
||||
// ... we want this false positive rate:
|
||||
dataUpdateTrackerFP = 0.99
|
||||
dataUpdateTrackerQueueSize = 10000
|
||||
|
||||
dataUpdateTrackerFilename = dataUsageBucket + SlashSeparator + ".tracker.bin"
|
||||
dataUpdateTrackerVersion = 1
|
||||
dataUpdateTrackerSaveInterval = 5 * time.Minute
|
||||
|
||||
// Reset bloom filters every n cycle
|
||||
dataUpdateTrackerResetEvery = 1000
|
||||
)
|
||||
|
||||
var (
|
||||
objectUpdatedCh chan<- string
|
||||
intDataUpdateTracker *dataUpdateTracker
|
||||
)
|
||||
|
||||
func init() {
|
||||
intDataUpdateTracker = newDataUpdateTracker()
|
||||
objectUpdatedCh = intDataUpdateTracker.input
|
||||
}
|
||||
|
||||
type dataUpdateTracker struct {
|
||||
mu sync.Mutex
|
||||
input chan string
|
||||
save chan struct{}
|
||||
debug bool
|
||||
saveExited chan struct{}
|
||||
|
||||
Current dataUpdateFilter
|
||||
History dataUpdateTrackerHistory
|
||||
Saved time.Time
|
||||
}
|
||||
|
||||
// newDataUpdateTracker returns a dataUpdateTracker with default settings.
|
||||
func newDataUpdateTracker() *dataUpdateTracker {
|
||||
d := &dataUpdateTracker{
|
||||
Current: dataUpdateFilter{
|
||||
idx: 1,
|
||||
},
|
||||
debug: env.Get(envDataUsageCrawlDebug, config.EnableOff) == config.EnableOn,
|
||||
input: make(chan string, dataUpdateTrackerQueueSize),
|
||||
save: make(chan struct{}, 1),
|
||||
saveExited: make(chan struct{}),
|
||||
}
|
||||
d.Current.bf = d.newBloomFilter()
|
||||
return d
|
||||
}
|
||||
|
||||
type dataUpdateTrackerHistory []dataUpdateFilter
|
||||
|
||||
type dataUpdateFilter struct {
|
||||
idx uint64
|
||||
bf bloomFilter
|
||||
}
|
||||
|
||||
type bloomFilter struct {
|
||||
*bloom.BloomFilter
|
||||
}
|
||||
|
||||
// emptyBloomFilter returns an empty bloom filter.
|
||||
func emptyBloomFilter() bloomFilter {
|
||||
return bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
||||
}
|
||||
|
||||
// containsDir returns whether the bloom filter contains a directory.
|
||||
// Note that objects in XL mode are also considered directories.
|
||||
func (b bloomFilter) containsDir(in string) bool {
|
||||
split := splitPathDeterministic(path.Clean(in))
|
||||
|
||||
if len(split) == 0 {
|
||||
return false
|
||||
}
|
||||
var tmp [dataUsageHashLen]byte
|
||||
hashPath(path.Join(split...)).bytes(tmp[:])
|
||||
return b.Test(tmp[:])
|
||||
}
|
||||
|
||||
// bytes returns the bloom filter serialized as a byte slice.
|
||||
func (b bloomFilter) bytes() []byte {
|
||||
if b.BloomFilter == nil {
|
||||
return nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, err := b.WriteTo(&buf)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// sort the dataUpdateTrackerHistory, newest first.
|
||||
// Returns whether the history is complete.
|
||||
func (d dataUpdateTrackerHistory) sort() bool {
|
||||
if len(d) == 0 {
|
||||
return true
|
||||
}
|
||||
sort.Slice(d, func(i, j int) bool {
|
||||
return d[i].idx > d[j].idx
|
||||
})
|
||||
return d[0].idx-d[len(d)-1].idx == uint64(len(d))
|
||||
}
|
||||
|
||||
// removeOlderThan will remove entries older than index 'n'.
|
||||
func (d *dataUpdateTrackerHistory) removeOlderThan(n uint64) {
|
||||
d.sort()
|
||||
dd := *d
|
||||
end := len(dd)
|
||||
for i := end - 1; i >= 0; i-- {
|
||||
if dd[i].idx < n {
|
||||
end = i
|
||||
}
|
||||
}
|
||||
dd = dd[:end]
|
||||
*d = dd
|
||||
}
|
||||
|
||||
// newBloomFilter returns a new bloom filter with default settings.
|
||||
func (d *dataUpdateTracker) newBloomFilter() bloomFilter {
|
||||
return bloomFilter{bloom.NewWithEstimates(dataUpdateTrackerEstItems, dataUpdateTrackerFP)}
|
||||
}
|
||||
|
||||
// current returns the current index.
|
||||
func (d *dataUpdateTracker) current() uint64 {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.Current.idx
|
||||
}
|
||||
|
||||
// start will load the current data from the drives start collecting information and
|
||||
// start a saver goroutine.
|
||||
// All of these will exit when the context is canceled.
|
||||
func (d *dataUpdateTracker) start(ctx context.Context, drives ...string) {
|
||||
if len(drives) <= 0 {
|
||||
logger.LogIf(ctx, errors.New("dataUpdateTracker.start: No drives specified"))
|
||||
return
|
||||
}
|
||||
d.load(ctx, drives...)
|
||||
go d.startCollector(ctx)
|
||||
go d.startSaver(ctx, dataUpdateTrackerSaveInterval, drives)
|
||||
}
|
||||
|
||||
// load will attempt to load data tracking information from the supplied drives.
|
||||
// The data will only be loaded if d.Saved is older than the one found on disk.
|
||||
// The newest working cache will be kept in d.
|
||||
// If no valid data usage tracker can be found d will remain unchanged.
|
||||
// If object is shared the caller should lock it.
|
||||
func (d *dataUpdateTracker) load(ctx context.Context, drives ...string) {
|
||||
if len(drives) <= 0 {
|
||||
logger.LogIf(ctx, errors.New("dataUpdateTracker.load: No drives specified"))
|
||||
return
|
||||
}
|
||||
for _, drive := range drives {
|
||||
|
||||
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
||||
f, err := os.Open(cacheFormatPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
err = d.deserialize(f, d.Saved)
|
||||
if err != nil && err != io.EOF {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// startSaver will start a saver that will write d to all supplied drives at specific intervals.
|
||||
// The saver will save and exit when supplied context is closed.
|
||||
func (d *dataUpdateTracker) startSaver(ctx context.Context, interval time.Duration, drives []string) {
|
||||
t := time.NewTicker(interval)
|
||||
defer t.Stop()
|
||||
var buf bytes.Buffer
|
||||
d.mu.Lock()
|
||||
saveNow := d.save
|
||||
exited := make(chan struct{})
|
||||
d.saveExited = exited
|
||||
d.mu.Unlock()
|
||||
defer close(exited)
|
||||
for {
|
||||
var exit bool
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
exit = true
|
||||
case <-t.C:
|
||||
case <-saveNow:
|
||||
}
|
||||
buf.Reset()
|
||||
d.mu.Lock()
|
||||
d.Saved = UTCNow()
|
||||
err := d.serialize(&buf)
|
||||
if d.debug {
|
||||
logger.Info(color.Green("dataUpdateTracker:")+" Saving: %v bytes, Current idx: %v", buf.Len(), d.Current.idx)
|
||||
}
|
||||
d.mu.Unlock()
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, "Error serializing usage tracker data")
|
||||
if exit {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
logger.LogIf(ctx, errors.New("zero sized output, skipping save"))
|
||||
continue
|
||||
}
|
||||
for _, drive := range drives {
|
||||
cacheFormatPath := pathJoin(drive, dataUpdateTrackerFilename)
|
||||
err := ioutil.WriteFile(cacheFormatPath, buf.Bytes(), os.ModePerm)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if exit {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serialize all data in d to dst.
|
||||
// Caller should hold lock if d is expected to be shared.
|
||||
// If an error is returned, there will likely be partial data written to dst.
|
||||
func (d *dataUpdateTracker) serialize(dst io.Writer) (err error) {
|
||||
ctx := GlobalContext
|
||||
var tmp [8]byte
|
||||
o := bufio.NewWriter(dst)
|
||||
defer func() {
|
||||
if err == nil {
|
||||
err = o.Flush()
|
||||
}
|
||||
}()
|
||||
|
||||
// Version
|
||||
if err := o.WriteByte(dataUpdateTrackerVersion); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Timestamp.
|
||||
binary.LittleEndian.PutUint64(tmp[:], uint64(d.Saved.Unix()))
|
||||
if _, err := o.Write(tmp[:]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Current
|
||||
binary.LittleEndian.PutUint64(tmp[:], d.Current.idx)
|
||||
if _, err := o.Write(tmp[:]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := d.Current.bf.WriteTo(o); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// History
|
||||
binary.LittleEndian.PutUint64(tmp[:], uint64(len(d.History)))
|
||||
if _, err := o.Write(tmp[:]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, bf := range d.History {
|
||||
// Current
|
||||
binary.LittleEndian.PutUint64(tmp[:], bf.idx)
|
||||
if _, err := o.Write(tmp[:]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := bf.bf.WriteTo(o); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deserialize will deserialize the supplied input if the input is newer than the supplied time.
|
||||
func (d *dataUpdateTracker) deserialize(src io.Reader, newerThan time.Time) error {
|
||||
ctx := GlobalContext
|
||||
var dst dataUpdateTracker
|
||||
var tmp [8]byte
|
||||
|
||||
// Version
|
||||
if _, err := io.ReadFull(src, tmp[:1]); err != nil {
|
||||
if d.debug {
|
||||
if err != io.EOF {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
switch tmp[0] {
|
||||
case dataUpdateTrackerVersion:
|
||||
default:
|
||||
return errors.New("dataUpdateTracker: Unknown data version")
|
||||
}
|
||||
// Timestamp.
|
||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
t := time.Unix(int64(binary.LittleEndian.Uint64(tmp[:])), 0)
|
||||
if !t.After(newerThan) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Current
|
||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst.Current.idx = binary.LittleEndian.Uint64(tmp[:])
|
||||
dst.Current.bf = emptyBloomFilter()
|
||||
if _, err := dst.Current.bf.ReadFrom(src); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// History
|
||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
n := binary.LittleEndian.Uint64(tmp[:])
|
||||
dst.History = make(dataUpdateTrackerHistory, int(n))
|
||||
for i, e := range dst.History {
|
||||
if _, err := io.ReadFull(src, tmp[:8]); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.idx = binary.LittleEndian.Uint64(tmp[:])
|
||||
e.bf = emptyBloomFilter()
|
||||
if _, err := e.bf.ReadFrom(src); err != nil {
|
||||
if d.debug {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst.History[i] = e
|
||||
}
|
||||
// Ignore what remains on the stream.
|
||||
// Update d:
|
||||
d.Current = dst.Current
|
||||
d.History = dst.History
|
||||
d.Saved = dst.Saved
|
||||
return nil
|
||||
}
|
||||
|
||||
// start a collector that picks up entries from objectUpdatedCh
|
||||
// and adds them to the current bloom filter.
|
||||
func (d *dataUpdateTracker) startCollector(ctx context.Context) {
|
||||
var tmp [dataUsageHashLen]byte
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case in := <-d.input:
|
||||
bucket, _ := path2BucketObjectWithBasePath("", in)
|
||||
if bucket == "" {
|
||||
if d.debug && len(in) > 0 {
|
||||
logger.Info(color.Green("data-usage:")+" no bucket (%s)", in)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if isReservedOrInvalidBucket(bucket, false) {
|
||||
if false && d.debug {
|
||||
logger.Info(color.Green("data-usage:")+" isReservedOrInvalidBucket: %v, entry: %v", bucket, in)
|
||||
}
|
||||
continue
|
||||
}
|
||||
split := splitPathDeterministic(in)
|
||||
|
||||
// Add all paths until level 3.
|
||||
d.mu.Lock()
|
||||
for i := range split {
|
||||
if d.debug && false {
|
||||
logger.Info(color.Green("dataUpdateTracker:") + " Marking path dirty: " + color.Blue(path.Join(split[:i+1]...)))
|
||||
}
|
||||
hashPath(path.Join(split[:i+1]...)).bytes(tmp[:])
|
||||
d.Current.bf.Add(tmp[:])
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find entry with specified index.
|
||||
// Returns nil if not found.
|
||||
func (d dataUpdateTrackerHistory) find(idx uint64) *dataUpdateFilter {
|
||||
for _, f := range d {
|
||||
if f.idx == idx {
|
||||
return &f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterFrom will return a combined bloom filter.
|
||||
func (d *dataUpdateTracker) filterFrom(ctx context.Context, oldest, newest uint64) *bloomFilterResponse {
|
||||
bf := d.newBloomFilter()
|
||||
bfr := bloomFilterResponse{
|
||||
OldestIdx: oldest,
|
||||
CurrentIdx: d.Current.idx,
|
||||
Complete: true,
|
||||
}
|
||||
// Loop through each index requested.
|
||||
for idx := oldest; idx <= newest; idx++ {
|
||||
v := d.History.find(idx)
|
||||
if v == nil {
|
||||
if d.Current.idx == idx {
|
||||
// Merge current.
|
||||
err := bf.Merge(d.Current.bf.BloomFilter)
|
||||
logger.LogIf(ctx, err)
|
||||
if err != nil {
|
||||
bfr.Complete = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
bfr.Complete = false
|
||||
bfr.OldestIdx = idx + 1
|
||||
continue
|
||||
}
|
||||
|
||||
err := bf.Merge(v.bf.BloomFilter)
|
||||
if err != nil {
|
||||
bfr.Complete = false
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
bfr.NewestIdx = idx
|
||||
}
|
||||
var dst bytes.Buffer
|
||||
_, err := bf.WriteTo(&dst)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return nil
|
||||
}
|
||||
bfr.Filter = dst.Bytes()
|
||||
|
||||
return &bfr
|
||||
}
|
||||
|
||||
// cycleFilter will cycle the bloom filter to start recording to index y if not already.
|
||||
// The response will contain a bloom filter starting at index x up to, but not including index y.
|
||||
// If y is 0, the response will not update y, but return the currently recorded information
|
||||
// from the up until and including current y.
|
||||
func (d *dataUpdateTracker) cycleFilter(ctx context.Context, oldest, current uint64) (*bloomFilterResponse, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if current == 0 {
|
||||
if len(d.History) == 0 {
|
||||
return d.filterFrom(ctx, d.Current.idx, d.Current.idx), nil
|
||||
}
|
||||
d.History.sort()
|
||||
return d.filterFrom(ctx, d.History[len(d.History)-1].idx, d.Current.idx), nil
|
||||
}
|
||||
|
||||
// Move current to history if new one requested
|
||||
if d.Current.idx != current {
|
||||
if d.debug {
|
||||
logger.Info(color.Green("dataUpdateTracker:")+" cycle bloom filter: %v -> %v", d.Current.idx, current)
|
||||
}
|
||||
|
||||
d.History = append(d.History, d.Current)
|
||||
d.Current.idx = current
|
||||
d.Current.bf = d.newBloomFilter()
|
||||
select {
|
||||
case d.save <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
d.History.removeOlderThan(oldest)
|
||||
return d.filterFrom(ctx, oldest, current), nil
|
||||
}
|
||||
|
||||
// splitPathDeterministic will split the provided relative path
|
||||
// deterministically and return up to the first 3 elements of the path.
|
||||
// Slash and dot prefixes are removed.
|
||||
// Trailing slashes are removed.
|
||||
// Returns 0 length if no parts are found after trimming.
|
||||
func splitPathDeterministic(in string) []string {
|
||||
split := strings.Split(in, SlashSeparator)
|
||||
|
||||
// Trim empty start/end
|
||||
for len(split) > 0 {
|
||||
if len(split[0]) > 0 && split[0] != "." {
|
||||
break
|
||||
}
|
||||
split = split[1:]
|
||||
}
|
||||
for len(split) > 0 {
|
||||
if len(split[len(split)-1]) > 0 {
|
||||
break
|
||||
}
|
||||
split = split[:len(split)-1]
|
||||
}
|
||||
|
||||
// Return up to 3 parts.
|
||||
if len(split) > 3 {
|
||||
split = split[:3]
|
||||
}
|
||||
return split
|
||||
}
|
||||
|
||||
// bloomFilterRequest request bloom filters.
|
||||
// Current index will be updated to current and entries back to Oldest is returned.
|
||||
type bloomFilterRequest struct {
|
||||
Oldest uint64
|
||||
Current uint64
|
||||
}
|
||||
|
||||
type bloomFilterResponse struct {
|
||||
// Current index being written to.
|
||||
CurrentIdx uint64
|
||||
// Oldest index in the returned bloom filter.
|
||||
OldestIdx uint64
|
||||
// Newest Index in the returned bloom filter.
|
||||
NewestIdx uint64
|
||||
// Are all indexes between oldest and newest filled?
|
||||
Complete bool
|
||||
// Binary data of the bloom filter.
|
||||
Filter []byte
|
||||
}
|
||||
|
||||
// ObjectPathUpdated indicates a path has been updated.
|
||||
// The function will never block.
|
||||
func ObjectPathUpdated(s string) {
|
||||
select {
|
||||
case objectUpdatedCh <- s:
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* 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"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/cmd/logger/message/log"
|
||||
)
|
||||
|
||||
type testLoggerI interface {
|
||||
Helper()
|
||||
Log(args ...interface{})
|
||||
}
|
||||
|
||||
type testingLogger struct {
|
||||
mu sync.Mutex
|
||||
t testLoggerI
|
||||
}
|
||||
|
||||
func (t *testingLogger) Send(entry interface{}, errKind string) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.t == nil {
|
||||
return nil
|
||||
}
|
||||
e, ok := entry.(log.Entry)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected log entry structure %#v", entry)
|
||||
}
|
||||
|
||||
t.t.Helper()
|
||||
t.t.Log(e.Level, ":", errKind, e.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func addTestingLogging(t testLoggerI) func() {
|
||||
tl := &testingLogger{t: t}
|
||||
logger.AddTarget(tl)
|
||||
return func() {
|
||||
tl.mu.Lock()
|
||||
defer tl.mu.Unlock()
|
||||
tl.t = nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataUpdateTracker(t *testing.T) {
|
||||
dut := newDataUpdateTracker()
|
||||
// Change some defaults.
|
||||
dut.debug = testing.Verbose()
|
||||
dut.input = make(chan string)
|
||||
dut.save = make(chan struct{})
|
||||
|
||||
defer addTestingLogging(t)()
|
||||
|
||||
dut.Current.bf = dut.newBloomFilter()
|
||||
|
||||
tmpDir, err := ioutil.TempDir("", "TestDataUpdateTracker")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Dir(filepath.Join(tmpDir, dataUpdateTrackerFilename)), os.ModePerm)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
dut.start(ctx, tmpDir)
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
check []string // if not empty, check against these instead.
|
||||
exist bool
|
||||
}{
|
||||
{
|
||||
in: "bucket/directory/file.txt",
|
||||
check: []string{"bucket", "bucket/", "/bucket", "bucket/directory", "bucket/directory/", "bucket/directory/file.txt", "/bucket/directory/file.txt"},
|
||||
exist: true,
|
||||
},
|
||||
{
|
||||
// System bucket
|
||||
in: ".minio.sys/ignoreme/pls",
|
||||
exist: false,
|
||||
},
|
||||
{
|
||||
// Not a valid bucket
|
||||
in: "./bucket/okfile.txt",
|
||||
check: []string{"./bucket/okfile.txt", "/bucket/okfile.txt", "bucket/okfile.txt"},
|
||||
exist: false,
|
||||
},
|
||||
{
|
||||
// Not a valid bucket
|
||||
in: "æ/okfile.txt",
|
||||
check: []string{"æ/okfile.txt", "æ/okfile.txt", "æ"},
|
||||
exist: false,
|
||||
},
|
||||
{
|
||||
in: "/bucket2/okfile2.txt",
|
||||
check: []string{"./bucket2/okfile2.txt", "/bucket2/okfile2.txt", "bucket2/okfile2.txt", "bucket2"},
|
||||
exist: true,
|
||||
},
|
||||
{
|
||||
in: "/bucket3/prefix/okfile2.txt",
|
||||
check: []string{"./bucket3/prefix/okfile2.txt", "/bucket3/prefix/okfile2.txt", "bucket3/prefix/okfile2.txt", "bucket3/prefix", "bucket3"},
|
||||
exist: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in, func(t *testing.T) {
|
||||
dut.input <- tt.in
|
||||
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
||||
dut.mu.Lock()
|
||||
defer dut.mu.Unlock()
|
||||
if len(tt.check) == 0 {
|
||||
got := dut.Current.bf.containsDir(tt.in)
|
||||
if got != tt.exist {
|
||||
// For unlimited tests this could lead to false positives,
|
||||
// but it should be deterministic.
|
||||
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, check := range tt.check {
|
||||
got := dut.Current.bf.containsDir(check)
|
||||
if got != tt.exist {
|
||||
// For unlimited tests this could lead to false positives,
|
||||
// but it should be deterministic.
|
||||
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
||||
}
|
||||
continue
|
||||
}
|
||||
})
|
||||
}
|
||||
// Cycle to history
|
||||
_, err = dut.cycleFilter(ctx, 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dut.input <- "cycle2/file.txt"
|
||||
dut.input <- "" // Sending empty string ensures the previous is added to filter.
|
||||
|
||||
tests = append(tests, struct {
|
||||
in string
|
||||
check []string
|
||||
exist bool
|
||||
}{in: "cycle2/file.txt", exist: true})
|
||||
|
||||
// Shut down
|
||||
cancel()
|
||||
<-dut.saveExited
|
||||
|
||||
if dut.current() != 2 {
|
||||
t.Fatal("wrong current idx after save. want 2, got:", dut.current())
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Reload...
|
||||
dut = newDataUpdateTracker()
|
||||
dut.start(ctx, tmpDir)
|
||||
|
||||
if dut.current() != 2 {
|
||||
t.Fatal("current idx after load not preserved. want 2, got:", dut.current())
|
||||
}
|
||||
bfr2, err := dut.cycleFilter(ctx, 1, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bfr2.Complete {
|
||||
t.Fatal("Wanted complete, didn't get it")
|
||||
}
|
||||
if bfr2.CurrentIdx != 3 {
|
||||
t.Fatal("wanted index 3, got", bfr2.CurrentIdx)
|
||||
}
|
||||
if bfr2.OldestIdx != 1 {
|
||||
t.Fatal("wanted oldest index 3, got", bfr2.OldestIdx)
|
||||
}
|
||||
|
||||
// Rerun test with returned bfr2
|
||||
bf := dut.newBloomFilter()
|
||||
_, err = bf.ReadFrom(bytes.NewBuffer(bfr2.Filter))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.in+"-reloaded", func(t *testing.T) {
|
||||
if len(tt.check) == 0 {
|
||||
got := bf.containsDir(tt.in)
|
||||
if got != tt.exist {
|
||||
// For unlimited tests this could lead to false positives,
|
||||
// but it should be deterministic.
|
||||
t.Errorf("entry %q, got: %v, want %v", tt.in, got, tt.exist)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, check := range tt.check {
|
||||
got := bf.containsDir(check)
|
||||
if got != tt.exist {
|
||||
// For unlimited tests this could lead to false positives,
|
||||
// but it should be deterministic.
|
||||
t.Errorf("entry %q, check: %q, got: %v, want %v", tt.in, check, got, tt.exist)
|
||||
}
|
||||
continue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDataUpdateTracker(b *testing.B) {
|
||||
dut := newDataUpdateTracker()
|
||||
// Change some defaults.
|
||||
dut.debug = false
|
||||
dut.input = make(chan string)
|
||||
dut.save = make(chan struct{})
|
||||
|
||||
defer addTestingLogging(b)()
|
||||
|
||||
dut.Current.bf = dut.newBloomFilter()
|
||||
// We do this unbuffered. This will very significantly reduce throughput, so this is a worst case.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go dut.startCollector(ctx)
|
||||
input := make([]string, 1000)
|
||||
rng := rand.New(rand.NewSource(0xabad1dea))
|
||||
tmp := []string{"bucket", "aprefix", "nextprefixlevel", "maybeobjname", "evendeeper", "ok-one-morelevel", "final.object"}
|
||||
for i := range input {
|
||||
tmp := tmp[:1+rng.Intn(cap(tmp)-1)]
|
||||
input[i] = path.Join(tmp...)
|
||||
}
|
||||
b.SetBytes(1)
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
dut.input <- input[rng.Intn(len(input))]
|
||||
}
|
||||
}
|
||||
+44
-6
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
@@ -62,9 +63,10 @@ type dataUsageEntryInfo struct {
|
||||
|
||||
type dataUsageCacheInfo struct {
|
||||
// Name of the bucket. Also root element.
|
||||
Name string
|
||||
LastUpdate time.Time
|
||||
NextCycle uint8
|
||||
Name string
|
||||
LastUpdate time.Time
|
||||
NextCycle uint32
|
||||
BloomFilter []byte `msg:"BloomFilter,omitempty"`
|
||||
}
|
||||
|
||||
// merge other data usage entry into this, excluding children.
|
||||
@@ -77,8 +79,8 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
||||
}
|
||||
|
||||
// mod returns true if the hash mod cycles == cycle.
|
||||
func (h dataUsageHash) mod(cycle uint8, cycles uint8) bool {
|
||||
return uint8(h)%cycles == cycle%cycles
|
||||
func (h dataUsageHash) mod(cycle uint32, cycles uint32) bool {
|
||||
return uint32(h)%cycles == cycle%cycles
|
||||
}
|
||||
|
||||
// addChildString will add a child based on its name.
|
||||
@@ -110,6 +112,7 @@ func (d *dataUsageCache) find(path string) *dataUsageEntry {
|
||||
}
|
||||
|
||||
// dui converts the flattened version of the path to DataUsageInfo.
|
||||
// As a side effect d will be flattened, use a clone if this is not ok.
|
||||
func (d *dataUsageCache) dui(path string, buckets []BucketInfo) DataUsageInfo {
|
||||
e := d.find(path)
|
||||
if e == nil {
|
||||
@@ -158,6 +161,32 @@ func (d *dataUsageCache) replaceHashed(hash dataUsageHash, parent *dataUsageHash
|
||||
}
|
||||
}
|
||||
|
||||
// copyWithChildren will copy entry with hash from src if it exists along with any children.
|
||||
// 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) copyWithChildren(src *dataUsageCache, hash dataUsageHash, parent *dataUsageHash) {
|
||||
if d.Cache == nil {
|
||||
d.Cache = make(map[dataUsageHash]dataUsageEntry, 100)
|
||||
}
|
||||
e, ok := src.Cache[hash]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
d.Cache[hash] = e
|
||||
for ch := range e.Children {
|
||||
if ch == hash {
|
||||
logger.LogIf(GlobalContext, errors.New("dataUsageCache.copyWithChildren: Circular reference"))
|
||||
return
|
||||
}
|
||||
d.copyWithChildren(src, ch, &hash)
|
||||
}
|
||||
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)
|
||||
@@ -167,6 +196,12 @@ func (d *dataUsageCache) StringAll() string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
// insert the hash into dst.
|
||||
// dst must be at least dataUsageHashLen bytes long.
|
||||
func (h dataUsageHash) bytes(dst []byte) {
|
||||
binary.LittleEndian.PutUint64(dst, uint64(h))
|
||||
}
|
||||
|
||||
// String returns a human readable representation of the string.
|
||||
func (h dataUsageHash) String() string {
|
||||
return fmt.Sprintf("%x", uint64(h))
|
||||
@@ -297,7 +332,7 @@ func (d *dataUsageCache) load(ctx context.Context, store ObjectLayer, name strin
|
||||
var buf bytes.Buffer
|
||||
err := store.GetObject(ctx, dataUsageBucket, name, 0, -1, &buf, "", ObjectOptions{})
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) {
|
||||
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
|
||||
return toObjectErr(err, dataUsageBucket, name)
|
||||
}
|
||||
*d = dataUsageCache{}
|
||||
@@ -325,6 +360,9 @@ func (d *dataUsageCache) save(ctx context.Context, store ObjectLayer, name strin
|
||||
name,
|
||||
NewPutObjReader(r, nil, nil),
|
||||
ObjectOptions{})
|
||||
if isErrBucketNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+66
-12
@@ -37,11 +37,17 @@ func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
return
|
||||
}
|
||||
case "NextCycle":
|
||||
z.NextCycle, err = dc.ReadUint8()
|
||||
z.NextCycle, err = dc.ReadUint32()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
case "BloomFilter":
|
||||
z.BloomFilter, err = dc.ReadBytes(z.BloomFilter)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BloomFilter")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -54,10 +60,24 @@ func (z *dataUsageCacheInfo) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 3
|
||||
func (z *dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(4)
|
||||
var zb0001Mask uint8 /* 4 bits */
|
||||
if z.BloomFilter == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x8
|
||||
}
|
||||
// variable map header, size zb0001Len
|
||||
err = en.Append(0x80 | uint8(zb0001Len))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if zb0001Len == 0 {
|
||||
return
|
||||
}
|
||||
// write "Name"
|
||||
err = en.Append(0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
err = en.Append(0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -81,27 +101,55 @@ func (z dataUsageCacheInfo) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteUint8(z.NextCycle)
|
||||
err = en.WriteUint32(z.NextCycle)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
if (zb0001Mask & 0x8) == 0 { // if not empty
|
||||
// write "BloomFilter"
|
||||
err = en.Append(0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteBytes(z.BloomFilter)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BloomFilter")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
func (z *dataUsageCacheInfo) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 3
|
||||
// omitempty: check for empty values
|
||||
zb0001Len := uint32(4)
|
||||
var zb0001Mask uint8 /* 4 bits */
|
||||
if z.BloomFilter == nil {
|
||||
zb0001Len--
|
||||
zb0001Mask |= 0x8
|
||||
}
|
||||
// variable map header, size zb0001Len
|
||||
o = append(o, 0x80|uint8(zb0001Len))
|
||||
if zb0001Len == 0 {
|
||||
return
|
||||
}
|
||||
// string "Name"
|
||||
o = append(o, 0x83, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
o = append(o, 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)
|
||||
o = msgp.AppendUint32(o, z.NextCycle)
|
||||
if (zb0001Mask & 0x8) == 0 { // if not empty
|
||||
// string "BloomFilter"
|
||||
o = append(o, 0xab, 0x42, 0x6c, 0x6f, 0x6f, 0x6d, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72)
|
||||
o = msgp.AppendBytes(o, z.BloomFilter)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,11 +184,17 @@ func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
return
|
||||
}
|
||||
case "NextCycle":
|
||||
z.NextCycle, bts, err = msgp.ReadUint8Bytes(bts)
|
||||
z.NextCycle, bts, err = msgp.ReadUint32Bytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "NextCycle")
|
||||
return
|
||||
}
|
||||
case "BloomFilter":
|
||||
z.BloomFilter, bts, err = msgp.ReadBytesBytes(bts, z.BloomFilter)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "BloomFilter")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -154,8 +208,8 @@ func (z *dataUsageCacheInfo) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
}
|
||||
|
||||
// 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
|
||||
func (z *dataUsageCacheInfo) Msgsize() (s int) {
|
||||
s = 1 + 5 + msgp.StringPrefixSize + len(z.Name) + 11 + msgp.TimeSize + 10 + msgp.Uint32Size + 12 + msgp.BytesPrefixSize + len(z.BloomFilter)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+99
-24
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
@@ -32,19 +33,23 @@ import (
|
||||
"github.com/minio/minio/pkg/color"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"github.com/minio/minio/pkg/hash"
|
||||
"github.com/willf/bloom"
|
||||
)
|
||||
|
||||
const (
|
||||
envDataUsageCrawlConf = "MINIO_DISK_USAGE_CRAWL_ENABLE"
|
||||
envDataUsageCrawlDelay = "MINIO_DISK_USAGE_CRAWL_DELAY"
|
||||
envDataUsageCrawlDebug = "MINIO_DISK_USAGE_CRAWL_DEBUG"
|
||||
|
||||
dataUsageRoot = SlashSeparator
|
||||
dataUsageBucket = minioMetaBucket + SlashSeparator + bucketMetaPrefix
|
||||
|
||||
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"
|
||||
dataUsageBloomName = ".bloomcycle.bin"
|
||||
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.
|
||||
)
|
||||
|
||||
@@ -56,6 +61,20 @@ func initDataUsageStats(ctx context.Context, objAPI ObjectLayer) {
|
||||
}
|
||||
|
||||
func runDataUsageInfo(ctx context.Context, objAPI ObjectLayer) {
|
||||
// Load current bloom cycle
|
||||
nextBloomCycle := intDataUpdateTracker.current() + 1
|
||||
var buf bytes.Buffer
|
||||
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageBloomName, 0, -1, &buf, "", ObjectOptions{})
|
||||
if err != nil {
|
||||
if !isErrObjectNotFound(err) && !isErrBucketNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
} else {
|
||||
if buf.Len() == 8 {
|
||||
nextBloomCycle = binary.LittleEndian.Uint64(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -64,9 +83,33 @@ func runDataUsageInfo(ctx context.Context, objAPI ObjectLayer) {
|
||||
// Wait before starting next cycle and wait on startup.
|
||||
results := make(chan DataUsageInfo, 1)
|
||||
go storeDataUsageInBackend(ctx, objAPI, results)
|
||||
err := objAPI.CrawlAndGetDataUsage(ctx, results)
|
||||
bf, err := globalNotificationSys.updateBloomFilter(ctx, nextBloomCycle)
|
||||
logger.LogIf(ctx, err)
|
||||
err = objAPI.CrawlAndGetDataUsage(ctx, bf, results)
|
||||
close(results)
|
||||
logger.LogIf(ctx, err)
|
||||
if err == nil {
|
||||
// Store new cycle...
|
||||
nextBloomCycle++
|
||||
if nextBloomCycle%dataUpdateTrackerResetEvery == 0 {
|
||||
if intDataUpdateTracker.debug {
|
||||
logger.Info(color.Green("runDataUsageInfo:") + " Resetting bloom filter for next runs.")
|
||||
}
|
||||
nextBloomCycle++
|
||||
}
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], nextBloomCycle)
|
||||
r, err := hash.NewReader(bytes.NewReader(tmp[:]), int64(len(tmp)), "", "", int64(len(tmp)), false)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageBloomName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||
if !isErrBucketNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +130,9 @@ func storeDataUsageInBackend(ctx context.Context, objAPI ObjectLayer, gui <-chan
|
||||
}
|
||||
|
||||
_, err = objAPI.PutObject(ctx, dataUsageBucket, dataUsageObjName, NewPutObjReader(r, nil, nil), ObjectOptions{})
|
||||
logger.LogIf(ctx, err)
|
||||
if !isErrBucketNotFound(err) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +141,7 @@ func loadDataUsageFromBackend(ctx context.Context, objAPI ObjectLayer) (DataUsag
|
||||
|
||||
err := objAPI.GetObject(ctx, dataUsageBucket, dataUsageObjName, 0, -1, &dataUsageInfoJSON, "", ObjectOptions{})
|
||||
if err != nil {
|
||||
if isErrObjectNotFound(err) {
|
||||
if isErrObjectNotFound(err) || isErrBucketNotFound(err) {
|
||||
return DataUsageInfo{}, nil
|
||||
}
|
||||
return DataUsageInfo{}, toObjectErr(err, dataUsageBucket, dataUsageObjName)
|
||||
@@ -130,6 +175,7 @@ type folderScanner struct {
|
||||
getSize getSizeFn
|
||||
oldCache dataUsageCache
|
||||
newCache dataUsageCache
|
||||
withFilter *bloomFilter
|
||||
waitForLowActiveIO func()
|
||||
|
||||
dataUsageCrawlMult float64
|
||||
@@ -164,12 +210,22 @@ func (f *folderScanner) scanQueuedLevels(ctx context.Context, folders []cachedFo
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
thisHash := hashPath(folder.name)
|
||||
|
||||
if _, ok := f.oldCache.Cache[thisHash]; f.withFilter != nil && ok {
|
||||
// If folder isn't in filter and we have data, skip it completely.
|
||||
if folder.name != dataUsageRoot && !f.withFilter.containsDir(folder.name) {
|
||||
f.newCache.copyWithChildren(&f.oldCache, thisHash, folder.parent)
|
||||
if f.dataUsageCrawlDebug {
|
||||
logger.Info(color.Green("data-usage:")+" Skipping non-updated folder: %v", folder.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -301,11 +357,14 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
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))
|
||||
}
|
||||
}()
|
||||
logPrefix := color.Green("data-usage: ")
|
||||
logSuffix := color.Blue(" - %v + %v", basePath, cache.Info.Name)
|
||||
if dataUsageDebug {
|
||||
defer func() {
|
||||
logger.Info(logPrefix+" Crawl time: %v"+logSuffix, time.Since(t))
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
if cache.Info.Name == "" {
|
||||
cache.Info.Name = dataUsageRoot
|
||||
@@ -329,8 +388,16 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
dataUsageCrawlDebug: dataUsageDebug,
|
||||
}
|
||||
|
||||
if len(cache.Info.BloomFilter) > 0 {
|
||||
s.withFilter = &bloomFilter{BloomFilter: &bloom.BloomFilter{}}
|
||||
_, err := s.withFilter.ReadFrom(bytes.NewBuffer(cache.Info.BloomFilter))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err, logPrefix+"Error reading bloom filter")
|
||||
s.withFilter = nil
|
||||
}
|
||||
}
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(color.Green("runDataUsageInfo:") + " Starting crawler master")
|
||||
logger.Info(logPrefix+"Start crawling. Bloom filter: %v"+logSuffix, s.withFilter != nil)
|
||||
}
|
||||
|
||||
done := ctx.Done()
|
||||
@@ -341,14 +408,8 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
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)
|
||||
logger.Info(logPrefix+"Cycle: %v, Entries: %v"+logSuffix, cache.Info.NextCycle, len(cache.Cache))
|
||||
}
|
||||
|
||||
// Always scan flattenLevels deep. Cache root is level 0.
|
||||
@@ -387,9 +448,10 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
continue
|
||||
}
|
||||
if du == nil {
|
||||
logger.LogIf(ctx, errors.New("data-usage: no disk usage provided"))
|
||||
logger.Info(logPrefix + "no disk usage provided" + logSuffix)
|
||||
continue
|
||||
}
|
||||
|
||||
s.newCache.replace(folder.name, "", *du)
|
||||
// Add to parent manually
|
||||
if folder.parent != nil {
|
||||
@@ -415,6 +477,17 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
continue
|
||||
}
|
||||
|
||||
if s.withFilter != nil {
|
||||
// If folder isn't in filter, skip it completely.
|
||||
if !s.withFilter.containsDir(folder.name) {
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"Skipping non-updated folder: %v"+logSuffix, folder)
|
||||
}
|
||||
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 {
|
||||
@@ -427,7 +500,9 @@ func updateUsage(ctx context.Context, basePath string, cache dataUsageCache, wai
|
||||
}
|
||||
s.newCache.replaceHashed(h, folder.parent, *du)
|
||||
}
|
||||
|
||||
if s.dataUsageCrawlDebug {
|
||||
logger.Info(logPrefix+"Finished crawl, %v entries"+logSuffix, len(s.newCache.Cache))
|
||||
}
|
||||
s.newCache.Info.LastUpdate = UTCNow()
|
||||
s.newCache.Info.NextCycle++
|
||||
return s.newCache, nil
|
||||
|
||||
+52
-37
@@ -21,14 +21,15 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/djherbis/atime"
|
||||
@@ -37,7 +38,6 @@ import (
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/disk"
|
||||
"github.com/minio/sio"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -125,17 +125,16 @@ func (m *cacheMeta) ToObjectInfo(bucket, object string) (o ObjectInfo) {
|
||||
|
||||
// represents disk cache struct
|
||||
type diskCache struct {
|
||||
dir string // caching directory
|
||||
quotaPct int // max usage in %
|
||||
// mark false if drive is offline
|
||||
online bool
|
||||
// mutex to protect updates to online variable
|
||||
onlineMutex *sync.RWMutex
|
||||
gcCounter uint64 // ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
// is set to 0 if drive is offline
|
||||
online uint32
|
||||
|
||||
dir string // caching directory
|
||||
quotaPct int // max usage in %
|
||||
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.
|
||||
@@ -153,8 +152,7 @@ func newDiskCache(dir string, quotaPct, after, lowWatermark, highWatermark int)
|
||||
after: after,
|
||||
lowWatermark: lowWatermark,
|
||||
highWatermark: highWatermark,
|
||||
online: true,
|
||||
onlineMutex: &sync.RWMutex{},
|
||||
online: 1,
|
||||
pool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
b := disk.AlignedBlock(int(cacheBlkSize))
|
||||
@@ -228,6 +226,10 @@ func (c *diskCache) toClear() uint64 {
|
||||
return bytesToClear(int64(di.Total), int64(di.Free), uint64(c.quotaPct), uint64(c.lowWatermark))
|
||||
}
|
||||
|
||||
var (
|
||||
errDoneForNow = errors.New("done for now")
|
||||
)
|
||||
|
||||
// Purge cache entries that were not accessed.
|
||||
func (c *diskCache) purge(ctx context.Context) {
|
||||
if c.diskUsageLow() {
|
||||
@@ -241,7 +243,7 @@ func (c *diskCache) purge(ctx context.Context) {
|
||||
// need to be cleaned up.
|
||||
expiry := UTCNow().Add(-cacheExpiryDays)
|
||||
// defaulting max hits count to 100
|
||||
scorer, err := newFileScorer(int64(toFree), time.Now().Unix(), 100)
|
||||
scorer, err := newFileScorer(toFree, time.Now().Unix(), 100)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
@@ -263,25 +265,24 @@ func (c *diskCache) purge(ctx context.Context) {
|
||||
}
|
||||
return fm
|
||||
}
|
||||
objDirs, err := ioutil.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, obj := range objDirs {
|
||||
if obj.Name() == minioMetaBucket {
|
||||
continue
|
||||
filterFn := func(name string, typ os.FileMode) error {
|
||||
if name == minioMetaBucket {
|
||||
// Proceed to next file.
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheDir := pathJoin(c.dir, obj.Name())
|
||||
cacheDir := pathJoin(c.dir, name)
|
||||
meta, _, numHits, err := c.statCachedMeta(ctx, cacheDir)
|
||||
if err != nil {
|
||||
// delete any partially filled cache entry left behind.
|
||||
removeAll(cacheDir)
|
||||
continue
|
||||
// Proceed to next file.
|
||||
return nil
|
||||
}
|
||||
|
||||
// stat all cached file ranges and cacheDataFile.
|
||||
cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, obj.Name()))
|
||||
cachedFiles := fiStatFn(meta.Ranges, cacheDataFile, pathJoin(c.dir, name))
|
||||
objInfo := meta.ToObjectInfo("", "")
|
||||
cc := cacheControlOpts(objInfo)
|
||||
for fname, fi := range cachedFiles {
|
||||
@@ -291,9 +292,11 @@ func (c *diskCache) purge(ctx context.Context) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
scorer.adjustSaveBytes(-fi.Size())
|
||||
|
||||
// break early if sufficient disk space reclaimed.
|
||||
if c.diskUsageLow() {
|
||||
return
|
||||
// if we found disk usage is already low, we return nil filtering is complete.
|
||||
return errDoneForNow
|
||||
}
|
||||
}
|
||||
continue
|
||||
@@ -305,12 +308,24 @@ func (c *diskCache) purge(ctx context.Context) {
|
||||
if err != nil || (fi.ModTime().Before(expiry) && len(cachedFiles) == 0) {
|
||||
removeAll(cacheDir)
|
||||
scorer.adjustSaveBytes(-fi.Size())
|
||||
continue
|
||||
// Proceed to next file.
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we found disk usage is already low, we return nil filtering is complete.
|
||||
if c.diskUsageLow() {
|
||||
return
|
||||
return errDoneForNow
|
||||
}
|
||||
|
||||
// Proceed to next file.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := readDirFilterFn(c.dir, filterFn); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, path := range scorer.fileNames() {
|
||||
removeAll(path)
|
||||
slashIdx := strings.LastIndex(path, SlashSeparator)
|
||||
@@ -323,27 +338,25 @@ func (c *diskCache) purge(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (c *diskCache) incGCCounter() {
|
||||
c.gcCounter.Add(uint64(1))
|
||||
atomic.AddUint64(&c.gcCounter, 1)
|
||||
}
|
||||
|
||||
func (c *diskCache) resetGCCounter() {
|
||||
c.gcCounter.Store(uint64(0))
|
||||
atomic.StoreUint64(&c.gcCounter, 0)
|
||||
}
|
||||
|
||||
func (c *diskCache) gcCount() uint64 {
|
||||
return c.gcCounter.Load()
|
||||
return atomic.LoadUint64(&c.gcCounter)
|
||||
}
|
||||
|
||||
// sets cache drive status
|
||||
func (c *diskCache) setOnline(status bool) {
|
||||
c.onlineMutex.Lock()
|
||||
c.online = status
|
||||
c.onlineMutex.Unlock()
|
||||
func (c *diskCache) setOffline() {
|
||||
atomic.StoreUint32(&c.online, 0)
|
||||
}
|
||||
|
||||
// returns true if cache drive is online
|
||||
func (c *diskCache) IsOnline() bool {
|
||||
c.onlineMutex.RLock()
|
||||
defer c.onlineMutex.RUnlock()
|
||||
return c.online
|
||||
return atomic.LoadUint32(&c.online) != 0
|
||||
}
|
||||
|
||||
// Stat returns ObjectInfo from disk cache
|
||||
@@ -663,7 +676,8 @@ func (c *diskCache) Put(ctx context.Context, bucket, object string, data io.Read
|
||||
}
|
||||
n, err := c.bitrotWriteToCache(cachePath, cacheDataFile, reader, actualSize)
|
||||
if IsErr(err, baseErrs...) {
|
||||
c.setOnline(false)
|
||||
// take the cache drive offline
|
||||
c.setOffline()
|
||||
}
|
||||
if err != nil {
|
||||
removeAll(cachePath)
|
||||
@@ -710,7 +724,8 @@ func (c *diskCache) putRange(ctx context.Context, bucket, object string, data io
|
||||
cacheFile := MustGetUUID()
|
||||
n, err := c.bitrotWriteToCache(cachePath, cacheFile, reader, actualSize)
|
||||
if IsErr(err, baseErrs...) {
|
||||
c.setOnline(false)
|
||||
// take the cache drive offline
|
||||
c.setOffline()
|
||||
}
|
||||
if err != nil {
|
||||
removeAll(cachePath)
|
||||
|
||||
+10
-12
@@ -16,46 +16,44 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
import "sync/atomic"
|
||||
|
||||
// CacheStats - represents bytes served from cache,
|
||||
// cache hits and cache misses.
|
||||
type CacheStats struct {
|
||||
BytesServed atomic.Uint64
|
||||
Hits atomic.Uint64
|
||||
Misses atomic.Uint64
|
||||
BytesServed uint64
|
||||
Hits uint64
|
||||
Misses uint64
|
||||
}
|
||||
|
||||
// Increase total bytes served from cache
|
||||
func (s *CacheStats) incBytesServed(n int64) {
|
||||
s.BytesServed.Add(uint64(n))
|
||||
atomic.AddUint64(&s.BytesServed, uint64(n))
|
||||
}
|
||||
|
||||
// Increase cache hit by 1
|
||||
func (s *CacheStats) incHit() {
|
||||
s.Hits.Add(uint64(1))
|
||||
atomic.AddUint64(&s.Hits, 1)
|
||||
}
|
||||
|
||||
// Increase cache miss by 1
|
||||
func (s *CacheStats) incMiss() {
|
||||
s.Misses.Add(uint64(1))
|
||||
atomic.AddUint64(&s.Misses, 1)
|
||||
}
|
||||
|
||||
// Get total bytes served
|
||||
func (s *CacheStats) getBytesServed() uint64 {
|
||||
return s.BytesServed.Load()
|
||||
return atomic.LoadUint64(&s.BytesServed)
|
||||
}
|
||||
|
||||
// Get total cache hits
|
||||
func (s *CacheStats) getHits() uint64 {
|
||||
return s.Hits.Load()
|
||||
return atomic.LoadUint64(&s.Hits)
|
||||
}
|
||||
|
||||
// Get total cache misses
|
||||
func (s *CacheStats) getMisses() uint64 {
|
||||
return s.Misses.Load()
|
||||
return atomic.LoadUint64(&s.Misses)
|
||||
}
|
||||
|
||||
// Prepare new CacheStats structure
|
||||
|
||||
+12
-8
@@ -285,7 +285,7 @@ func isMetadataSame(m1, m2 map[string]string) bool {
|
||||
}
|
||||
|
||||
type fileScorer struct {
|
||||
saveBytes int64
|
||||
saveBytes uint64
|
||||
now int64
|
||||
maxHits int
|
||||
// 1/size for consistent score.
|
||||
@@ -294,21 +294,21 @@ type fileScorer struct {
|
||||
// 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
|
||||
queuedBytes uint64
|
||||
}
|
||||
|
||||
type queuedFile struct {
|
||||
name string
|
||||
size int64
|
||||
size uint64
|
||||
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")
|
||||
func newFileScorer(saveBytes uint64, 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")
|
||||
@@ -325,7 +325,7 @@ func (f *fileScorer) addFile(name string, lastAccess time.Time, size int64, hits
|
||||
// Calculate how much we want to delete this object.
|
||||
file := queuedFile{
|
||||
name: name,
|
||||
size: size,
|
||||
size: uint64(size),
|
||||
}
|
||||
score := float64(f.now - lastAccess.Unix())
|
||||
// Size as fraction of how much we want to save, 0->1.
|
||||
@@ -353,7 +353,11 @@ func (f *fileScorer) addFile(name string, lastAccess time.Time, size int64, hits
|
||||
// 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 n < 0 {
|
||||
f.saveBytes -= ^uint64(n - 1)
|
||||
} else {
|
||||
f.saveBytes += uint64(n)
|
||||
}
|
||||
if f.saveBytes <= 0 {
|
||||
f.queue.Init()
|
||||
f.saveBytes = 0
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestGetCachedLoc(t *testing.T) {
|
||||
// find cache drive where object would be hashed
|
||||
index := c.hashIndex(bucketName, objectName)
|
||||
// turn off drive by setting online status to false
|
||||
c.cache[index].online = false
|
||||
c.cache[index].setOffline()
|
||||
cfs, err := c.getCacheLoc(bucketName, objectName)
|
||||
if n == 1 && err == errDiskNotFound {
|
||||
continue
|
||||
@@ -118,7 +118,7 @@ func TestGetCacheMaxUse(t *testing.T) {
|
||||
// find cache drive where object would be hashed
|
||||
index := c.hashIndex(bucketName, objectName)
|
||||
// turn off drive by setting online status to false
|
||||
c.cache[index].online = false
|
||||
c.cache[index].setOffline()
|
||||
cb, err := c.getCacheLoc(bucketName, objectName)
|
||||
if n == 1 && err == errDiskNotFound {
|
||||
continue
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
@@ -59,12 +58,6 @@ func (api objectAPIHandlers) GetBucketReplicationHandler(w http.ResponseWriter,
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DeleteBucketTaggingHandler - DELETE bucket tagging, a dummy api
|
||||
func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DeleteBucketWebsiteHandler - DELETE bucket website, a dummy api
|
||||
func (api objectAPIHandlers) DeleteBucketWebsiteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
@@ -130,41 +123,3 @@ func (api objectAPIHandlers) GetBucketCorsHandler(w http.ResponseWriter, r *http
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// GetBucketTaggingHandler - GET bucket tagging, a dummy api
|
||||
func (api objectAPIHandlers) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketTagging")
|
||||
|
||||
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 getBucketTagging 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 bucket exists, before proceeding further...
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
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))
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
@@ -247,22 +247,6 @@ func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, m
|
||||
return newEncryptReader(content, key, bucket, object, metadata, crypto.S3.IsRequested(r.Header))
|
||||
}
|
||||
|
||||
// DecryptCopyRequest decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptCopyRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
var (
|
||||
key []byte
|
||||
err error
|
||||
)
|
||||
if crypto.SSECopy.IsRequested(r.Header) {
|
||||
key, err = ParseSSECopyCustomerRequest(r.Header, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return newDecryptWriter(client, key, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||
switch {
|
||||
default:
|
||||
@@ -317,14 +301,6 @@ func decryptObjectInfo(key []byte, bucket, object string, metadata map[string]st
|
||||
}
|
||||
}
|
||||
|
||||
func newDecryptWriter(client io.Writer, key []byte, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
objectEncryptionKey, err := decryptObjectInfo(key, bucket, object, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newDecryptWriterWithObjectKey(client, objectEncryptionKey, seqNumber, metadata)
|
||||
}
|
||||
|
||||
func newDecryptWriterWithObjectKey(client io.Writer, objectEncryptionKey []byte, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
writer, err := sio.DecryptWriter(client, sio.Config{
|
||||
Key: objectEncryptionKey,
|
||||
@@ -450,26 +426,6 @@ func DecryptBlocksRequestR(inputReader io.Reader, h http.Header, offset,
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// DecryptRequestWithSequenceNumber decrypts the object with the client provided key. It also removes
|
||||
// the client-side-encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequestWithSequenceNumber(client io.Writer, r *http.Request, bucket, object string, seqNumber uint32, metadata map[string]string) (io.WriteCloser, error) {
|
||||
if crypto.S3.IsEncrypted(metadata) {
|
||||
return newDecryptWriter(client, nil, bucket, object, seqNumber, metadata)
|
||||
}
|
||||
|
||||
key, err := ParseSSECustomerRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newDecryptWriter(client, key, bucket, object, seqNumber, metadata)
|
||||
}
|
||||
|
||||
// DecryptRequest decrypts the object with client provided key for SSE-C and SSE-S3. It also removes
|
||||
// the encryption metadata from the object and sets the correct headers.
|
||||
func DecryptRequest(client io.Writer, r *http.Request, bucket, object string, metadata map[string]string) (io.WriteCloser, error) {
|
||||
return DecryptRequestWithSequenceNumber(client, r, bucket, object, 0, metadata)
|
||||
}
|
||||
|
||||
// DecryptBlocksReader - decrypts multipart parts, while implementing
|
||||
// a io.Reader compatible interface.
|
||||
type DecryptBlocksReader struct {
|
||||
|
||||
@@ -78,135 +78,6 @@ func TestEncryptRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var decryptRequestTests = []struct {
|
||||
bucket, object string
|
||||
header map[string]string
|
||||
metadata map[string]string
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||
crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||
crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: crypto.SealAlgorithm,
|
||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: false,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: "HMAC-SHA3",
|
||||
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||
crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "XAm0dRrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
{
|
||||
bucket: "bucket",
|
||||
object: "object-2",
|
||||
header: map[string]string{
|
||||
crypto.SSECAlgorithm: "AES256",
|
||||
crypto.SSECKey: "MzJieXRlc2xvbmdzZWNyZXRrZXltdXN0cHJvdmlkZWQ=",
|
||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||
},
|
||||
metadata: map[string]string{
|
||||
crypto.SSESealAlgorithm: crypto.SealAlgorithm,
|
||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||
},
|
||||
shouldFail: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDecryptRequest(t *testing.T) {
|
||||
defer func(flag bool) { globalIsSSL = flag }(globalIsSSL)
|
||||
globalIsSSL = true
|
||||
for i, test := range decryptRequestTests[1:] {
|
||||
client := bytes.NewBuffer(nil)
|
||||
req := &http.Request{Header: http.Header{}}
|
||||
for k, v := range test.header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
_, err := DecryptRequest(client, req, test.bucket, test.object, test.metadata)
|
||||
if err != nil && !test.shouldFail {
|
||||
t.Fatalf("Test %d: Failed to encrypt request: %v", i, err)
|
||||
}
|
||||
if err == nil && test.shouldFail {
|
||||
t.Fatalf("Test %d: should fail but passed", i)
|
||||
}
|
||||
if key, ok := test.metadata[crypto.SSECKey]; ok {
|
||||
t.Errorf("Test %d: Client provided key survived in metadata - key: %s", i, key)
|
||||
}
|
||||
if kdf, ok := test.metadata[crypto.SSESealAlgorithm]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKDF should not be part of metadata: %v", i, kdf)
|
||||
}
|
||||
if iv, ok := test.metadata[crypto.SSEIV]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: crypto.SSEIV should not be part of metadata: %v", i, iv)
|
||||
}
|
||||
if mac, ok := test.metadata[crypto.SSECSealedKey]; ok && !test.shouldFail {
|
||||
t.Errorf("Test %d: ServerSideEncryptionKeyMAC should not be part of metadata: %v", i, mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptObjectInfoTests = []struct {
|
||||
info ObjectInfo
|
||||
headers http.Header
|
||||
|
||||
+87
-24
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -63,11 +64,72 @@ var isValidSetSize = func(count uint64) bool {
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
|
||||
}
|
||||
|
||||
func commonSetDriveCount(divisibleSize uint64, setCounts []uint64) (setSize uint64) {
|
||||
// prefers setCounts to be sorted for optimal behavior.
|
||||
if divisibleSize < setCounts[len(setCounts)-1] {
|
||||
return divisibleSize
|
||||
}
|
||||
|
||||
// Figure out largest value of total_drives_in_erasure_set which results
|
||||
// in least number of total_drives/total_drives_erasure_set ratio.
|
||||
prevD := divisibleSize / setCounts[0]
|
||||
for _, cnt := range setCounts {
|
||||
if divisibleSize%cnt == 0 {
|
||||
d := divisibleSize / cnt
|
||||
if d <= prevD {
|
||||
prevD = d
|
||||
setSize = cnt
|
||||
}
|
||||
}
|
||||
}
|
||||
return setSize
|
||||
}
|
||||
|
||||
// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
|
||||
// input argument patterns, the symmetry calculation is to ensure that
|
||||
// we also use uniform number of drives common across all ellipses patterns.
|
||||
func possibleSetCountsWithSymmetry(setCounts []uint64, argPatterns []ellipses.ArgPattern) []uint64 {
|
||||
var newSetCounts = make(map[uint64]struct{})
|
||||
for _, ss := range setCounts {
|
||||
var symmetry bool
|
||||
for _, argPattern := range argPatterns {
|
||||
for _, p := range argPattern {
|
||||
if uint64(len(p.Seq)) > ss {
|
||||
symmetry = uint64(len(p.Seq))%ss == 0
|
||||
} else {
|
||||
symmetry = ss%uint64(len(p.Seq)) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// With no arg patterns, it is expected that user knows
|
||||
// the right symmetry, so either ellipses patterns are
|
||||
// provided (recommended) or no ellipses patterns.
|
||||
if _, ok := newSetCounts[ss]; !ok && (symmetry || argPatterns == nil) {
|
||||
newSetCounts[ss] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
setCounts = []uint64{}
|
||||
for setCount := range newSetCounts {
|
||||
setCounts = append(setCounts, setCount)
|
||||
}
|
||||
|
||||
// Not necessarily needed but it ensures to the readers
|
||||
// eyes that we prefer a sorted setCount slice for the
|
||||
// subsequent function to figure out the right common
|
||||
// divisor, it avoids loops.
|
||||
sort.Slice(setCounts, func(i, j int) bool {
|
||||
return setCounts[i] < setCounts[j]
|
||||
})
|
||||
|
||||
return setCounts
|
||||
}
|
||||
|
||||
// getSetIndexes returns list of indexes which provides the set size
|
||||
// on each index, this function also determines the final set size
|
||||
// The final set size has the affinity towards choosing smaller
|
||||
// indexes (total sets)
|
||||
func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint64) (setIndexes [][]uint64, err error) {
|
||||
func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
|
||||
if len(totalSizes) == 0 || len(args) == 0 {
|
||||
return nil, errInvalidArgument
|
||||
}
|
||||
@@ -81,24 +143,7 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
}
|
||||
}
|
||||
|
||||
var setSize uint64
|
||||
|
||||
commonSize := getDivisibleSize(totalSizes)
|
||||
if commonSize > setSizes[len(setSizes)-1] {
|
||||
prevD := commonSize / setSizes[0]
|
||||
for _, i := range setSizes {
|
||||
if commonSize%i == 0 {
|
||||
d := commonSize / i
|
||||
if d <= prevD {
|
||||
prevD = d
|
||||
setSize = i
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setSize = commonSize
|
||||
}
|
||||
|
||||
possibleSetCounts := func(setSize uint64) (ss []uint64) {
|
||||
for _, s := range setSizes {
|
||||
if setSize%s == 0 {
|
||||
@@ -114,11 +159,11 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
return nil, config.ErrInvalidNumberOfErasureEndpoints(nil).Msg(msg)
|
||||
}
|
||||
|
||||
var setSize uint64
|
||||
// Custom set drive count allows to override automatic distribution.
|
||||
// only meant if you want to further optimize drive distribution.
|
||||
if customSetDriveCount > 0 {
|
||||
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)
|
||||
}
|
||||
var found bool
|
||||
for _, ss := range setCounts {
|
||||
if ss == customSetDriveCount {
|
||||
@@ -128,7 +173,16 @@ func getSetIndexes(args []string, totalSizes []uint64, customSetDriveCount uint6
|
||||
if !found {
|
||||
return nil, config.ErrInvalidErasureSetSize(nil).Msg(msg)
|
||||
}
|
||||
|
||||
// No automatic symmetry calculation expected, user is on their own
|
||||
setSize = customSetDriveCount
|
||||
globalCustomErasureDriveCount = true
|
||||
} else {
|
||||
// Returns possible set counts with symmetry.
|
||||
setCounts = possibleSetCountsWithSymmetry(setCounts, argPatterns)
|
||||
|
||||
// Final set size with all the symmetry accounted for.
|
||||
setSize = commonSetDriveCount(commonSize, setCounts)
|
||||
}
|
||||
|
||||
// Check whether setSize is with the supported range.
|
||||
@@ -202,7 +256,7 @@ func parseEndpointSet(customSetDriveCount uint64, args ...string) (ep endpointSe
|
||||
argPatterns[i] = patterns
|
||||
}
|
||||
|
||||
ep.setIndexes, err = getSetIndexes(args, getTotalSizes(argPatterns), customSetDriveCount)
|
||||
ep.setIndexes, err = getSetIndexes(args, getTotalSizes(argPatterns), customSetDriveCount, argPatterns)
|
||||
if err != nil {
|
||||
return endpointSet{}, config.ErrInvalidErasureEndpoints(nil).Msg(err.Error())
|
||||
}
|
||||
@@ -224,7 +278,7 @@ func GetAllSets(customSetDriveCount uint64, args ...string) ([][]string, error)
|
||||
// Check if we have more one args.
|
||||
if len(args) > 1 {
|
||||
var err error
|
||||
setIndexes, err = getSetIndexes(args, []uint64{uint64(len(args))}, customSetDriveCount)
|
||||
setIndexes, err = getSetIndexes(args, []uint64{uint64(len(args))}, customSetDriveCount, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -258,6 +312,15 @@ func GetAllSets(customSetDriveCount uint64, args ...string) ([][]string, error)
|
||||
return setArgs, nil
|
||||
}
|
||||
|
||||
// Override set drive count for manual distribution.
|
||||
const (
|
||||
EnvErasureSetDriveCount = "MINIO_ERASURE_SET_DRIVE_COUNT"
|
||||
)
|
||||
|
||||
var (
|
||||
globalCustomErasureDriveCount = false
|
||||
)
|
||||
|
||||
// CreateServerEndpoints - validates and creates new endpoints from input args, supports
|
||||
// both ellipses and without ellipses transparently.
|
||||
func createServerEndpoints(serverAddr string, args ...string) (
|
||||
@@ -268,7 +331,7 @@ func createServerEndpoints(serverAddr string, args ...string) (
|
||||
return nil, -1, -1, errInvalidArgument
|
||||
}
|
||||
|
||||
if v := env.Get("MINIO_ERASURE_SET_DRIVE_COUNT", ""); v != "" {
|
||||
if v := env.Get(EnvErasureSetDriveCount, ""); v != "" {
|
||||
setDriveCount, err = strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, -1, -1, config.ErrInvalidErasureSetSize(err)
|
||||
|
||||
@@ -104,11 +104,18 @@ func TestGetSetIndexesEnvOverride(t *testing.T) {
|
||||
8,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...2}/data{1...180}"},
|
||||
[]uint64{360},
|
||||
[][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
|
||||
15,
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...12}/data{1...12}"},
|
||||
[]uint64{144},
|
||||
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16}},
|
||||
16,
|
||||
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
|
||||
12,
|
||||
true,
|
||||
},
|
||||
{
|
||||
@@ -160,7 +167,16 @@ func TestGetSetIndexesEnvOverride(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, testCase.envOverride)
|
||||
var argPatterns = make([]ellipses.ArgPattern, len(testCase.args))
|
||||
for i, arg := range testCase.args {
|
||||
patterns, err := ellipses.FindEllipsesPatterns(arg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected failure %s", err)
|
||||
}
|
||||
argPatterns[i] = patterns
|
||||
}
|
||||
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, testCase.envOverride, argPatterns)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Expected success but failed instead %s", err)
|
||||
}
|
||||
@@ -202,6 +218,24 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
[][]uint64{{9, 9, 9}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...3}/data{1...180}"},
|
||||
[]uint64{540},
|
||||
[][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...2}.rack{1...4}/data{1...180}"},
|
||||
[]uint64{1440},
|
||||
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...2}/data{1...180}"},
|
||||
[]uint64{360},
|
||||
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
|
||||
[]uint64{4, 8, 12},
|
||||
@@ -243,7 +277,15 @@ func TestGetSetIndexes(t *testing.T) {
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, 0)
|
||||
var argPatterns = make([]ellipses.ArgPattern, len(testCase.args))
|
||||
for i, arg := range testCase.args {
|
||||
patterns, err := ellipses.FindEllipsesPatterns(arg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected failure %s", err)
|
||||
}
|
||||
argPatterns[i] = patterns
|
||||
}
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, 0, argPatterns)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Expected success but failed instead %s", err)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ func (a badDisk) ReadFileStream(volume, path string, offset, length int64) (io.R
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
func (a badDisk) UpdateBloomFilter(ctx context.Context, oldest, current uint64) (*bloomFilterResponse, error) {
|
||||
return nil, errFaultyDisk
|
||||
}
|
||||
|
||||
func (a badDisk) CreateFile(volume, path string, size int64, reader io.Reader) error {
|
||||
return errFaultyDisk
|
||||
}
|
||||
|
||||
+7
-6
@@ -190,7 +190,7 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
||||
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
||||
|
||||
// Add a deployment ID, if it does not exist.
|
||||
if err := formatFSFixDeploymentID(fsFormatPath); err != nil {
|
||||
if err := formatFSFixDeploymentID(ctx, fsFormatPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func formatFSGetDeploymentID(rlk *lock.RLockedFile) (id string, err error) {
|
||||
}
|
||||
|
||||
// Generate a deployment ID if one does not exist already.
|
||||
func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||
func formatFSFixDeploymentID(ctx context.Context, fsFormatPath string) error {
|
||||
rlk, err := lock.RLockedOpenFile(fsFormatPath)
|
||||
if err == nil {
|
||||
// format.json can be empty in a rare condition when another
|
||||
@@ -339,11 +339,12 @@ func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||
return time.Now().Round(time.Second).Sub(formatStartTime).String()
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
defer close(doneCh)
|
||||
retryCtx, cancel := context.WithCancel(ctx)
|
||||
// Indicate to our routine to exit cleanly upon return.
|
||||
defer cancel()
|
||||
|
||||
var wlk *lock.LockedFile
|
||||
retryCh := newRetryTimerSimple(doneCh)
|
||||
retryCh := newRetryTimerSimple(retryCtx)
|
||||
var stop bool
|
||||
for !stop {
|
||||
select {
|
||||
@@ -358,7 +359,7 @@ func formatFSFixDeploymentID(fsFormatPath string) error {
|
||||
return err
|
||||
}
|
||||
stop = true
|
||||
case <-globalOSSignalCh:
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("Initializing FS format stopped gracefully")
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -456,7 +456,10 @@ func checkFormatXLValues(formats []*formatXLV3, drivesPerSet int) 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 {
|
||||
// Only if custom erasure drive count is set,
|
||||
// we should fail here other proceed to honor what
|
||||
// is present on the disk.
|
||||
if globalCustomErasureDriveCount && 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,6 +504,7 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
if _, err := fs.statBucketDir(ctx, bucket); err != nil {
|
||||
return oi, toObjectErr(err, bucket)
|
||||
}
|
||||
defer ObjectPathUpdated(pathutil.Join(bucket, object))
|
||||
|
||||
uploadIDDir := fs.getUploadIDDir(bucket, object, uploadID)
|
||||
// Just check if the uploadID exists to avoid copy if it doesn't.
|
||||
@@ -681,13 +682,6 @@ func (fs *FSObjects) CompleteMultipartUpload(ctx context.Context, bucket string,
|
||||
return oi, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if isWORMEnabled(bucket) {
|
||||
if _, err := fsStatFile(ctx, pathJoin(fs.fsPath, bucket, object)); err == nil {
|
||||
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
|
||||
}
|
||||
}
|
||||
|
||||
err = fsRenameFile(ctx, appendFilePath, pathJoin(fs.fsPath, bucket, object))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
|
||||
+36
-17
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -34,13 +35,14 @@ import (
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/minio-go/v6/pkg/s3utils"
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
"github.com/minio/minio/cmd/config"
|
||||
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/lifecycle"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/color"
|
||||
"github.com/minio/minio/pkg/lock"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
"github.com/minio/minio/pkg/mimedb"
|
||||
@@ -129,8 +131,12 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
var err error
|
||||
if fsPath, err = getValidPath(fsPath); err != nil {
|
||||
if err == errMinDiskSize {
|
||||
return nil, err
|
||||
return nil, config.ErrUnableToWriteInBackend(err).Hint(err.Error())
|
||||
} else if err == errUnsupportedDisk {
|
||||
hint := fmt.Sprintf("'%s' does not support O_DIRECT flags, refusing to use", fsPath)
|
||||
return nil, config.ErrUnsupportedBackend(err).Hint(hint)
|
||||
}
|
||||
|
||||
// Show a descriptive error with a hint about how to fix it.
|
||||
var username string
|
||||
if u, err := user.Current(); err == nil {
|
||||
@@ -180,6 +186,7 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
fs.fsFormatRlk = rlk
|
||||
|
||||
go fs.cleanupStaleMultipartUploads(ctx, GlobalMultipartCleanupInterval, GlobalMultipartExpiry)
|
||||
go intDataUpdateTracker.start(GlobalContext, fsPath)
|
||||
|
||||
// Return successfully initialized object layer.
|
||||
return fs, nil
|
||||
@@ -233,7 +240,7 @@ func (fs *FSObjects) waitForLowActiveIO() {
|
||||
}
|
||||
|
||||
// CrawlAndGetDataUsage returns data usage stats of the current FS deployment
|
||||
func (fs *FSObjects) CrawlAndGetDataUsage(ctx context.Context, updates chan<- DataUsageInfo) error {
|
||||
func (fs *FSObjects) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error {
|
||||
// Load bucket totals
|
||||
var oldCache dataUsageCache
|
||||
err := oldCache.load(ctx, fs, dataUsageCacheName)
|
||||
@@ -247,6 +254,15 @@ func (fs *FSObjects) CrawlAndGetDataUsage(ctx context.Context, updates chan<- Da
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldCache.Info.BloomFilter = nil
|
||||
if bf != nil {
|
||||
oldCache.Info.BloomFilter = bf.bytes()
|
||||
}
|
||||
|
||||
if false && intDataUpdateTracker.debug {
|
||||
b, _ := json.MarshalIndent(bf, "", " ")
|
||||
logger.Info("Bloom filter: %v", string(b))
|
||||
}
|
||||
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.
|
||||
@@ -256,11 +272,19 @@ func (fs *FSObjects) CrawlAndGetDataUsage(ctx context.Context, updates chan<- Da
|
||||
}
|
||||
return fi.Size(), nil
|
||||
})
|
||||
cache.Info.BloomFilter = nil
|
||||
|
||||
// Even if there was an error, the new cache may have better info.
|
||||
if cache.Info.LastUpdate.After(oldCache.Info.LastUpdate) {
|
||||
if intDataUpdateTracker.debug {
|
||||
logger.Info(color.Green("CrawlAndGetDataUsage:")+" Saving cache with %d entries", len(cache.Cache))
|
||||
}
|
||||
logger.LogIf(ctx, cache.save(ctx, fs, dataUsageCacheName))
|
||||
updates <- cache.dui(dataUsageRoot, buckets)
|
||||
} else {
|
||||
if intDataUpdateTracker.debug {
|
||||
logger.Info(color.Green("CrawlAndGetDataUsage:")+" Cache not updated, %d entries", len(cache.Cache))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -452,6 +476,8 @@ func (fs *FSObjects) DeleteBucket(ctx context.Context, bucket string, forceDelet
|
||||
// update metadata.
|
||||
func (fs *FSObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, e error) {
|
||||
cpSrcDstSame := isStringEqual(pathJoin(srcBucket, srcObject), pathJoin(dstBucket, dstObject))
|
||||
defer ObjectPathUpdated(path.Join(dstBucket, dstObject))
|
||||
|
||||
if !cpSrcDstSame {
|
||||
objectDWLock := fs.NewNSLock(ctx, dstBucket, dstObject)
|
||||
if err := objectDWLock.GetLock(globalObjectTimeout); err != nil {
|
||||
@@ -871,6 +897,7 @@ func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string
|
||||
if err := checkPutObjectArgs(ctx, bucket, object, fs, r.Size()); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
|
||||
// Lock the object.
|
||||
objectLock := fs.NewNSLock(ctx, bucket, object)
|
||||
if err := objectLock.GetLock(globalObjectTimeout); err != nil {
|
||||
@@ -878,6 +905,7 @@ func (fs *FSObjects) PutObject(ctx context.Context, bucket string, object string
|
||||
return objInfo, err
|
||||
}
|
||||
defer objectLock.Unlock()
|
||||
defer ObjectPathUpdated(path.Join(bucket, object))
|
||||
|
||||
atomic.AddInt64(&fs.activeIOCount, 1)
|
||||
defer func() {
|
||||
@@ -991,12 +1019,6 @@ func (fs *FSObjects) putObject(ctx context.Context, bucket string, object string
|
||||
|
||||
// Entire object was written to the temp location, now it's safe to rename it to the actual location.
|
||||
fsNSObjPath := pathJoin(fs.fsPath, bucket, object)
|
||||
// Deny if WORM is enabled
|
||||
if isWORMEnabled(bucket) {
|
||||
if _, err := fsStatFile(ctx, fsNSObjPath); err == nil {
|
||||
return ObjectInfo{}, ObjectAlreadyExists{Bucket: bucket, Object: object}
|
||||
}
|
||||
}
|
||||
if err = fsRenameFile(ctx, fsTmpObjPath, fsNSObjPath); err != nil {
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
@@ -1042,6 +1064,8 @@ func (fs *FSObjects) DeleteObject(ctx context.Context, bucket, object string) er
|
||||
return err
|
||||
}
|
||||
|
||||
defer ObjectPathUpdated(path.Join(bucket, object))
|
||||
|
||||
atomic.AddInt64(&fs.activeIOCount, 1)
|
||||
defer func() {
|
||||
atomic.AddInt64(&fs.activeIOCount, -1)
|
||||
@@ -1199,18 +1223,13 @@ func (fs *FSObjects) ListObjects(ctx context.Context, bucket, prefix, marker, de
|
||||
}
|
||||
|
||||
// GetObjectTag - get object tags from an existing object
|
||||
func (fs *FSObjects) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||
func (fs *FSObjects) GetObjectTag(ctx context.Context, bucket, object string) (*tags.Tags, error) {
|
||||
oi, err := fs.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
return tagging.Tagging{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags, err := tagging.FromString(oi.UserTags)
|
||||
if err != nil {
|
||||
return tagging.Tagging{}, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
return tags.ParseObjectTags(oi.UserTags)
|
||||
}
|
||||
|
||||
// PutObjectTag - replace or add tags to an existing object
|
||||
|
||||
+10
-1
@@ -131,6 +131,14 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get())
|
||||
logger.FatalIf(err, "Failed to read root CAs (%v)", err)
|
||||
|
||||
globalMinioEndpoint = func() string {
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(host, globalMinioPort))
|
||||
}()
|
||||
|
||||
// Handle gateway specific env
|
||||
gatewayHandleEnvVars()
|
||||
|
||||
@@ -168,10 +176,11 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
}
|
||||
|
||||
enableIAMOps := globalEtcdClient != nil
|
||||
enableBucketQuotaOps := env.Get(envDataUsageCrawlConf, config.EnableOn) == config.EnableOn
|
||||
|
||||
// Enable IAM admin APIs if etcd is enabled, if not just enable basic
|
||||
// operations such as profiling, server info etc.
|
||||
registerAdminRouter(router, enableConfigOps, enableIAMOps)
|
||||
registerAdminRouter(router, enableConfigOps, enableIAMOps, enableBucketQuotaOps)
|
||||
|
||||
// Add healthcheck router
|
||||
registerHealthCheckRouter(router)
|
||||
|
||||
+15
-16
@@ -18,57 +18,56 @@ package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// RequestStats - counts for Get and Head requests
|
||||
type RequestStats struct {
|
||||
Get atomic.Uint64 `json:"Get"`
|
||||
Head atomic.Uint64 `json:"Head"`
|
||||
Put atomic.Uint64 `json:"Put"`
|
||||
Post atomic.Uint64 `json:"Post"`
|
||||
Get uint64 `json:"Get"`
|
||||
Head uint64 `json:"Head"`
|
||||
Put uint64 `json:"Put"`
|
||||
Post uint64 `json:"Post"`
|
||||
}
|
||||
|
||||
// Metrics - represents bytes served from backend
|
||||
// only implemented for S3 Gateway
|
||||
type Metrics struct {
|
||||
bytesReceived atomic.Uint64
|
||||
bytesSent atomic.Uint64
|
||||
bytesReceived uint64
|
||||
bytesSent uint64
|
||||
requestStats RequestStats
|
||||
}
|
||||
|
||||
// IncBytesReceived - Increase total bytes received from gateway backend
|
||||
func (s *Metrics) IncBytesReceived(n uint64) {
|
||||
s.bytesReceived.Add(n)
|
||||
atomic.AddUint64(&s.bytesReceived, n)
|
||||
}
|
||||
|
||||
// GetBytesReceived - Get total bytes received from gateway backend
|
||||
func (s *Metrics) GetBytesReceived() uint64 {
|
||||
return s.bytesReceived.Load()
|
||||
return atomic.LoadUint64(&s.bytesReceived)
|
||||
}
|
||||
|
||||
// IncBytesSent - Increase total bytes sent to gateway backend
|
||||
func (s *Metrics) IncBytesSent(n uint64) {
|
||||
s.bytesSent.Add(n)
|
||||
atomic.AddUint64(&s.bytesSent, n)
|
||||
}
|
||||
|
||||
// GetBytesSent - Get total bytes received from gateway backend
|
||||
func (s *Metrics) GetBytesSent() uint64 {
|
||||
return s.bytesSent.Load()
|
||||
return atomic.LoadUint64(&s.bytesSent)
|
||||
}
|
||||
|
||||
// IncRequests - Increase request count sent to gateway backend by 1
|
||||
func (s *Metrics) IncRequests(method string) {
|
||||
// Only increment for Head & Get requests, else no op
|
||||
if method == http.MethodGet {
|
||||
s.requestStats.Get.Add(1)
|
||||
atomic.AddUint64(&s.requestStats.Get, 1)
|
||||
} else if method == http.MethodHead {
|
||||
s.requestStats.Head.Add(1)
|
||||
atomic.AddUint64(&s.requestStats.Head, 1)
|
||||
} else if method == http.MethodPut {
|
||||
s.requestStats.Put.Add(1)
|
||||
atomic.AddUint64(&s.requestStats.Put, 1)
|
||||
} else if method == http.MethodPost {
|
||||
s.requestStats.Post.Add(1)
|
||||
atomic.AddUint64(&s.requestStats.Post, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@ func printGatewayStartupMessage(apiEndPoints []string, backendType string) {
|
||||
|
||||
// SSL is configured reads certification chain, prints
|
||||
// authority and expiry.
|
||||
if globalIsSSL {
|
||||
printCertificateMsg(globalPublicCerts)
|
||||
if color.IsTerminal() && !globalCLIContext.Anonymous {
|
||||
if globalIsSSL {
|
||||
printCertificateMsg(globalPublicCerts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
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"
|
||||
@@ -50,7 +50,7 @@ func NewGatewayLayerWithLocker(gwLayer ObjectLayer) ObjectLayer {
|
||||
type GatewayUnsupported struct{}
|
||||
|
||||
// CrawlAndGetDataUsage - crawl is not implemented for gateway
|
||||
func (a GatewayUnsupported) CrawlAndGetDataUsage(ctx context.Context, updates chan<- DataUsageInfo) error {
|
||||
func (a GatewayUnsupported) CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error {
|
||||
logger.CriticalIf(ctx, errors.New("not implemented"))
|
||||
return NotImplemented{}
|
||||
}
|
||||
@@ -205,9 +205,9 @@ func (a GatewayUnsupported) PutObjectTag(ctx context.Context, bucket, object str
|
||||
}
|
||||
|
||||
// GetObjectTag - not implemented.
|
||||
func (a GatewayUnsupported) GetObjectTag(ctx context.Context, bucket, object string) (tagging.Tagging, error) {
|
||||
func (a GatewayUnsupported) GetObjectTag(ctx context.Context, bucket, object string) (*tags.Tags, error) {
|
||||
logger.LogIf(ctx, NotImplemented{})
|
||||
return tagging.Tagging{}, NotImplemented{}
|
||||
return nil, NotImplemented{}
|
||||
}
|
||||
|
||||
// DeleteObjectTag - not implemented.
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/minio/minio/pkg/env"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -48,16 +49,23 @@ import (
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
var (
|
||||
azureUploadChunkSize = getUploadChunkSizeFromEnv(azureChunkSizeEnvVar, strconv.Itoa(azureDefaultUploadChunkSize/humanize.MiByte))
|
||||
azureSdkTimeout = time.Duration(azureUploadChunkSize/humanize.MiByte) * azureSdkTimeoutPerMb
|
||||
azureUploadConcurrency = azureUploadMaxMemoryUsage / azureUploadChunkSize
|
||||
)
|
||||
|
||||
const (
|
||||
// The defaultDialTimeout for communicating with the cloud backends is set
|
||||
// to 30 seconds in utils.go; the Azure SDK recommends to set a timeout of 60
|
||||
// seconds per MB of data a client expects to upload so we must transfer less
|
||||
// than 0.5 MB per chunk to stay within the defaultDialTimeout tolerance.
|
||||
// See https://github.com/Azure/azure-storage-blob-go/blob/fc70003/azblob/zc_policy_retry.go#L39-L44 for more details.
|
||||
azureUploadChunkSize = 0.25 * humanize.MiByte
|
||||
azureSdkTimeout = (azureUploadChunkSize / humanize.MiByte) * 60 * time.Second
|
||||
azureUploadMaxMemoryUsage = 10 * humanize.MiByte
|
||||
azureUploadConcurrency = azureUploadMaxMemoryUsage / azureUploadChunkSize
|
||||
// To change the upload chunk size, set the environmental variable MINIO_AZURE_CHUNK_SIZE_MB with a (float) value between 0 and 100
|
||||
azureDefaultUploadChunkSize = 25 * humanize.MiByte
|
||||
azureSdkTimeoutPerMb = 60 * time.Second
|
||||
azureUploadMaxMemoryUsage = 100 * humanize.MiByte
|
||||
azureChunkSizeEnvVar = "MINIO_AZURE_CHUNK_SIZE_MB"
|
||||
|
||||
azureDownloadRetryAttempts = 5
|
||||
azureBlockSize = 100 * humanize.MiByte
|
||||
@@ -86,6 +94,7 @@ EXAMPLES:
|
||||
1. Start minio gateway server for Azure Blob Storage backend on custom endpoint.
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_ACCESS_KEY{{.AssignmentOperator}}azureaccountname
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_SECRET_KEY{{.AssignmentOperator}}azureaccountkey
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_AZURE_CHUNK_SIZE_MB {{.AssignmentOperator}}0.25
|
||||
{{.Prompt}} {{.HelpName}} https://azureaccountname.blob.custom.azure.endpoint
|
||||
|
||||
2. Start minio gateway server for Azure Blob Storage backend with edge caching enabled.
|
||||
@@ -97,6 +106,7 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_AFTER{{.AssignmentOperator}}3
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_LOW{{.AssignmentOperator}}75
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_CACHE_WATERMARK_HIGH{{.AssignmentOperator}}85
|
||||
{{.Prompt}} {{.EnvVarSetCommand}} MINIO_AZURE_CHUNK_SIZE_MB {{.AssignmentOperator}}25
|
||||
{{.Prompt}} {{.HelpName}}
|
||||
`
|
||||
|
||||
@@ -125,6 +135,27 @@ func azureGatewayMain(ctx *cli.Context) {
|
||||
minio.StartGateway(ctx, &Azure{host})
|
||||
}
|
||||
|
||||
// getUploadChunkSizeFromEnv returns the parsed chunk size from the environmental variable 'MINIO_AZURE_CHUNK_SIZE_MB'
|
||||
// The environmental variable should be a floating point number between 0 and 100 representing the MegaBytes
|
||||
// The returned value is an int representing the size in bytes
|
||||
func getUploadChunkSizeFromEnv(envvar string, defaultValue string) int {
|
||||
envChunkSize := env.Get(envvar, defaultValue)
|
||||
|
||||
i, err := strconv.ParseFloat(envChunkSize, 64)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return azureDefaultUploadChunkSize
|
||||
}
|
||||
|
||||
if i <= 0 || i > 100 {
|
||||
logger.LogIf(context.Background(), fmt.Errorf("ENV '%v' should be a floating point value between 0 and 100.\n"+
|
||||
"The upload chunk size is set to its default: %s\n", azureChunkSizeEnvVar, defaultValue))
|
||||
return azureDefaultUploadChunkSize
|
||||
}
|
||||
|
||||
return int(i * humanize.MiByte)
|
||||
}
|
||||
|
||||
// Azure implements Gateway.
|
||||
type Azure struct {
|
||||
host string
|
||||
|
||||
@@ -19,8 +19,11 @@ package azure
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/dustin/go-humanize"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
@@ -301,3 +304,36 @@ func TestCheckAzureUploadID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsingUploadChunkSize(t *testing.T) {
|
||||
key := "MINIO_AZURE_CHUNK_SIZE_MB"
|
||||
invalidValues := []string{
|
||||
"",
|
||||
"0,3",
|
||||
"100.1",
|
||||
"-1",
|
||||
}
|
||||
|
||||
for i, chunkValue := range invalidValues {
|
||||
os.Setenv(key, chunkValue)
|
||||
result := getUploadChunkSizeFromEnv(key, strconv.Itoa(azureDefaultUploadChunkSize/humanize.MiByte))
|
||||
if result != azureDefaultUploadChunkSize {
|
||||
t.Errorf("Test %d: expected: %d, got: %d", i+1, azureDefaultUploadChunkSize, result)
|
||||
}
|
||||
}
|
||||
|
||||
validValues := []string{
|
||||
"1",
|
||||
"1.25",
|
||||
"50",
|
||||
"99",
|
||||
}
|
||||
for i, chunkValue := range validValues {
|
||||
os.Setenv(key, chunkValue)
|
||||
result := getUploadChunkSizeFromEnv(key, strconv.Itoa(azureDefaultUploadChunkSize/humanize.MiByte))
|
||||
if result == azureDefaultUploadChunkSize {
|
||||
t.Errorf("Test %d: expected: %d, got: %d", i+1, azureDefaultUploadChunkSize, result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017 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 b2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
// Test b2 object error.
|
||||
func TestB2ObjectError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
params []string
|
||||
b2Err error
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
[]string{}, nil, nil,
|
||||
},
|
||||
{
|
||||
[]string{}, fmt.Errorf("Not *Error"), fmt.Errorf("Not *Error"),
|
||||
},
|
||||
{
|
||||
[]string{}, fmt.Errorf("Non B2 Error"), fmt.Errorf("Non B2 Error"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualErr := b2ToObjectError(testCase.b2Err, testCase.params...)
|
||||
if actualErr != nil {
|
||||
if actualErr.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test b2 msg code to object error.
|
||||
func TestB2MsgCodeToObjectError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
params []string
|
||||
code int
|
||||
msgCode string
|
||||
msg string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
[]string{"bucket"},
|
||||
1,
|
||||
"duplicate_bucket_name",
|
||||
"",
|
||||
minio.BucketAlreadyOwnedByYou{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"},
|
||||
1,
|
||||
"bad_request",
|
||||
"",
|
||||
minio.BucketNotFound{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"},
|
||||
1,
|
||||
"bad_request",
|
||||
"",
|
||||
minio.ObjectNameInvalid{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"},
|
||||
1,
|
||||
"bad_bucket_id",
|
||||
"",
|
||||
minio.BucketNotFound{Bucket: "bucket"},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"},
|
||||
1,
|
||||
"file_not_present",
|
||||
"",
|
||||
minio.ObjectNotFound{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object"},
|
||||
1,
|
||||
"not_found",
|
||||
"",
|
||||
minio.ObjectNotFound{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket"},
|
||||
1,
|
||||
"cannot_delete_non_empty_bucket",
|
||||
"",
|
||||
minio.BucketNotEmpty{
|
||||
Bucket: "bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"bucket", "object", "uploadID"},
|
||||
1,
|
||||
"",
|
||||
"No active upload for",
|
||||
minio.InvalidUploadID{
|
||||
UploadID: "uploadID",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
actualErr := b2MsgCodeToObjectError(testCase.code, testCase.msgCode, testCase.msg, testCase.params...)
|
||||
if actualErr != nil {
|
||||
if actualErr.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2017 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.
|
||||
@@ -17,15 +17,23 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
// Import all gateways.
|
||||
_ "github.com/minio/minio/cmd/gateway/azure"
|
||||
_ "github.com/minio/minio/cmd/gateway/gcs"
|
||||
_ "github.com/minio/minio/cmd/gateway/hdfs"
|
||||
// Import all gateways please keep the order
|
||||
|
||||
// NAS
|
||||
_ "github.com/minio/minio/cmd/gateway/nas"
|
||||
|
||||
// Azure
|
||||
_ "github.com/minio/minio/cmd/gateway/azure"
|
||||
|
||||
// S3
|
||||
_ "github.com/minio/minio/cmd/gateway/s3"
|
||||
|
||||
// B2 is specifically kept here to avoid re-ordering by goimports,
|
||||
// please ask on github.com/minio/minio/issues before changing this.
|
||||
_ "github.com/minio/minio/cmd/gateway/b2"
|
||||
// Add your gateway here.
|
||||
// HDFS
|
||||
_ "github.com/minio/minio/cmd/gateway/hdfs"
|
||||
|
||||
// GCS (use only if you must, GCS already supports S3 API)
|
||||
_ "github.com/minio/minio/cmd/gateway/gcs"
|
||||
// gateway functionality is frozen, no new gateways are being implemented
|
||||
// or considered for upstream inclusion at this point in time. if needed
|
||||
// please keep a fork of the project.
|
||||
)
|
||||
|
||||
+21
-9
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/http/stats"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/rs/cors"
|
||||
@@ -490,7 +491,6 @@ var notImplementedBucketResourceNames = map[string]bool{
|
||||
"metrics": true,
|
||||
"replication": true,
|
||||
"requestPayment": true,
|
||||
"tagging": true,
|
||||
"versioning": true,
|
||||
"website": true,
|
||||
}
|
||||
@@ -535,12 +535,21 @@ func setHTTPStatsHandler(h http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func (h httpStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
isS3Request := !strings.HasPrefix(r.URL.Path, minioReservedBucketPath)
|
||||
// record s3 connection stats.
|
||||
r.Body = &recordTrafficRequest{ReadCloser: r.Body, isS3Request: isS3Request}
|
||||
recordResponse := &recordTrafficResponse{ResponseWriter: w, isS3Request: isS3Request}
|
||||
// Meters s3 connection stats.
|
||||
meteredRequest := &stats.IncomingTrafficMeter{ReadCloser: r.Body}
|
||||
meteredResponse := &stats.OutgoingTrafficMeter{ResponseWriter: w}
|
||||
|
||||
// Execute the request
|
||||
h.handler.ServeHTTP(recordResponse, r)
|
||||
r.Body = meteredRequest
|
||||
h.handler.ServeHTTP(meteredResponse, r)
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, minioReservedBucketPath) {
|
||||
globalConnStats.incInputBytes(meteredRequest.BytesCount())
|
||||
globalConnStats.incOutputBytes(meteredResponse.BytesCount())
|
||||
} else {
|
||||
globalConnStats.incS3InputBytes(meteredRequest.BytesCount())
|
||||
globalConnStats.incS3OutputBytes(meteredResponse.BytesCount())
|
||||
}
|
||||
}
|
||||
|
||||
// requestValidityHandler validates all the incoming paths for
|
||||
@@ -670,9 +679,12 @@ func (f bucketForwardingHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
bucket, object := request2BucketObjectName(r)
|
||||
|
||||
// ListBucket requests should be handled at current endpoint as
|
||||
// all buckets data can be fetched from here.
|
||||
if r.Method == http.MethodGet && bucket == "" && object == "" {
|
||||
// Requests in federated setups for STS type calls which are
|
||||
// performed at '/' resource should be routed by the muxer,
|
||||
// the assumption is simply such that requests without a bucket
|
||||
// in a federated setup cannot be proxied, so serve them at
|
||||
// current server.
|
||||
if bucket == "" {
|
||||
f.handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
+21
-13
@@ -18,7 +18,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -38,7 +37,7 @@ var leaderLockTimeout = newDynamicTimeout(time.Minute, time.Minute)
|
||||
func newBgHealSequence(numDisks int) *healSequence {
|
||||
|
||||
reqInfo := &logger.ReqInfo{API: "BackgroundHeal"}
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
ctx, cancelCtx := context.WithCancel(logger.SetReqInfo(GlobalContext, reqInfo))
|
||||
|
||||
hs := madmin.HealOpts{
|
||||
// Remove objects that do not have read-quorum
|
||||
@@ -48,6 +47,7 @@ func newBgHealSequence(numDisks int) *healSequence {
|
||||
|
||||
return &healSequence{
|
||||
sourceCh: make(chan healSource),
|
||||
respCh: make(chan healResult),
|
||||
startTime: UTCNow(),
|
||||
clientToken: bgHealingUUID,
|
||||
settings: hs,
|
||||
@@ -55,15 +55,13 @@ func newBgHealSequence(numDisks int) *healSequence {
|
||||
Summary: healNotStartedStatus,
|
||||
HealSettings: hs,
|
||||
NumDisks: numDisks,
|
||||
updateLock: &sync.RWMutex{},
|
||||
},
|
||||
traverseAndHealDoneCh: make(chan error),
|
||||
stopSignalCh: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
reportProgress: false,
|
||||
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healFailedItemsMap: make(map[string]int64),
|
||||
cancelCtx: cancelCtx,
|
||||
ctx: ctx,
|
||||
reportProgress: false,
|
||||
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healFailedItemsMap: make(map[string]int64),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +93,12 @@ func healErasureSet(ctx context.Context, setIndex int, xlObj *xlObjects) error {
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Heal all buckets with all objects
|
||||
@@ -125,7 +128,7 @@ func deepHealObject(objectPath string) {
|
||||
|
||||
bgSeq.sourceCh <- healSource{
|
||||
path: objectPath,
|
||||
opts: &madmin.HealOpts{ScanMode: madmin.HealDeepScan},
|
||||
opts: madmin.HealOpts{ScanMode: madmin.HealDeepScan},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,12 @@ func execLeaderTasks(ctx context.Context, z *xlZones) {
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
|
||||
+7
-3
@@ -36,6 +36,7 @@ import (
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||
|
||||
"github.com/minio/minio/pkg/certs"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/pubsub"
|
||||
@@ -138,6 +139,8 @@ var (
|
||||
globalMinioPort = globalMinioDefaultPort
|
||||
// Holds the host that was passed using --address
|
||||
globalMinioHost = ""
|
||||
// Holds the possible host endpoint.
|
||||
globalMinioEndpoint = ""
|
||||
|
||||
// globalConfigSys server config system.
|
||||
globalConfigSys *ConfigSys
|
||||
@@ -213,11 +216,11 @@ var (
|
||||
globalOperationTimeout = newDynamicTimeout(10*time.Minute /*30*/, 600*time.Second) // default timeout for general ops
|
||||
globalHealingTimeout = newDynamicTimeout(30*time.Minute /*1*/, 30*time.Minute) // timeout for healing related ops
|
||||
|
||||
// Is worm enabled
|
||||
globalWORMEnabled bool
|
||||
|
||||
globalBucketObjectLockConfig = objectlock.NewBucketObjectLockConfig()
|
||||
|
||||
globalBucketQuotaSys *BucketQuotaSys
|
||||
globalBucketStorageCache bucketStorageCache
|
||||
|
||||
// Disk cache drives
|
||||
globalCacheConfig cache.Config
|
||||
|
||||
@@ -288,6 +291,7 @@ var (
|
||||
func getGlobalInfo() (globalInfo map[string]interface{}) {
|
||||
globalInfo = map[string]interface{}{
|
||||
"serverRegion": globalServerRegion,
|
||||
"domains": globalDomainNames,
|
||||
// Add more relevant global settings here.
|
||||
}
|
||||
|
||||
|
||||
+7
-44
@@ -28,11 +28,11 @@ import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
xhttp "github.com/minio/minio/cmd/http"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/minio/minio/pkg/auth"
|
||||
"github.com/minio/minio/pkg/bucket/object/tagging"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
)
|
||||
@@ -160,26 +160,6 @@ func extractMetadataFromMap(ctx context.Context, v map[string][]string, m map[st
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractTags extracts tag key and value from given http header. It then
|
||||
// - Parses the input format X-Amz-Tagging:"Key1=Value1&Key2=Value2" into a map[string]string
|
||||
// with entries in the format X-Amg-Tag-Key1:Value1, X-Amz-Tag-Key2:Value2
|
||||
// - Validates the tags
|
||||
// - Returns the Tag in original string format "Key1=Value1&Key2=Value2"
|
||||
func extractTags(ctx context.Context, tags string) (string, error) {
|
||||
// Check if the metadata has tagging related header
|
||||
if tags != "" {
|
||||
tagging, err := tagging.FromString(tags)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := tagging.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tagging.String(), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// The Query string for the redirect URL the client is
|
||||
// redirected on successful upload.
|
||||
func getRedirectPostRawQuery(objInfo ObjectInfo) string {
|
||||
@@ -360,36 +340,19 @@ func httpTraceHdrs(f http.HandlerFunc) http.HandlerFunc {
|
||||
|
||||
func collectAPIStats(api string, f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
globalHTTPStats.currentS3Requests.Inc(api)
|
||||
defer globalHTTPStats.currentS3Requests.Dec(api)
|
||||
|
||||
isS3Request := !strings.HasPrefix(r.URL.Path, minioReservedBucketPath)
|
||||
statsWriter := logger.NewResponseWriter(w)
|
||||
|
||||
// Time start before the call is about to start.
|
||||
tBefore := UTCNow()
|
||||
|
||||
apiStatsWriter := &recordAPIStats{ResponseWriter: w, TTFB: tBefore, isS3Request: isS3Request}
|
||||
|
||||
if isS3Request {
|
||||
globalHTTPStats.currentS3Requests.Inc(api)
|
||||
}
|
||||
|
||||
// Execute the request
|
||||
f.ServeHTTP(apiStatsWriter, r)
|
||||
|
||||
if isS3Request {
|
||||
globalHTTPStats.currentS3Requests.Dec(api)
|
||||
}
|
||||
|
||||
// Firstbyte read.
|
||||
tAfter := apiStatsWriter.TTFB
|
||||
f.ServeHTTP(statsWriter, r)
|
||||
|
||||
// Time duration in secs since the call started.
|
||||
//
|
||||
// We don't need to do nanosecond precision in this
|
||||
// simply for the fact that it is not human readable.
|
||||
durationSecs := tAfter.Sub(tBefore).Seconds()
|
||||
durationSecs := time.Since(statsWriter.StartTime).Seconds()
|
||||
|
||||
// Update http statistics
|
||||
globalHTTPStats.updateStats(api, r, apiStatsWriter, durationSecs)
|
||||
globalHTTPStats.updateStats(api, r, statsWriter, durationSecs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+19
-18
@@ -21,60 +21,61 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
// ConnStats - Network statistics
|
||||
// Count total input/output transferred bytes during
|
||||
// the server's life.
|
||||
type ConnStats struct {
|
||||
totalInputBytes atomic.Uint64
|
||||
totalOutputBytes atomic.Uint64
|
||||
s3InputBytes atomic.Uint64
|
||||
s3OutputBytes atomic.Uint64
|
||||
totalInputBytes uint64
|
||||
totalOutputBytes uint64
|
||||
s3InputBytes uint64
|
||||
s3OutputBytes uint64
|
||||
}
|
||||
|
||||
// Increase total input bytes
|
||||
func (s *ConnStats) incInputBytes(n int) {
|
||||
s.totalInputBytes.Add(uint64(n))
|
||||
atomic.AddUint64(&s.totalInputBytes, uint64(n))
|
||||
}
|
||||
|
||||
// Increase total output bytes
|
||||
func (s *ConnStats) incOutputBytes(n int) {
|
||||
s.totalOutputBytes.Add(uint64(n))
|
||||
atomic.AddUint64(&s.totalOutputBytes, uint64(n))
|
||||
}
|
||||
|
||||
// Return total input bytes
|
||||
func (s *ConnStats) getTotalInputBytes() uint64 {
|
||||
return s.totalInputBytes.Load()
|
||||
return atomic.LoadUint64(&s.totalInputBytes)
|
||||
}
|
||||
|
||||
// Return total output bytes
|
||||
func (s *ConnStats) getTotalOutputBytes() uint64 {
|
||||
return s.totalOutputBytes.Load()
|
||||
return atomic.LoadUint64(&s.totalOutputBytes)
|
||||
}
|
||||
|
||||
// Increase outbound input bytes
|
||||
func (s *ConnStats) incS3InputBytes(n int) {
|
||||
s.s3InputBytes.Add(uint64(n))
|
||||
atomic.AddUint64(&s.s3InputBytes, uint64(n))
|
||||
}
|
||||
|
||||
// Increase outbound output bytes
|
||||
func (s *ConnStats) incS3OutputBytes(n int) {
|
||||
s.s3OutputBytes.Add(uint64(n))
|
||||
atomic.AddUint64(&s.s3OutputBytes, uint64(n))
|
||||
}
|
||||
|
||||
// Return outbound input bytes
|
||||
func (s *ConnStats) getS3InputBytes() uint64 {
|
||||
return s.s3InputBytes.Load()
|
||||
return atomic.LoadUint64(&s.s3InputBytes)
|
||||
}
|
||||
|
||||
// Return outbound output bytes
|
||||
func (s *ConnStats) getS3OutputBytes() uint64 {
|
||||
return s.s3OutputBytes.Load()
|
||||
return atomic.LoadUint64(&s.s3OutputBytes)
|
||||
}
|
||||
|
||||
// Return connection stats (total input/output bytes and total s3 input/output bytes)
|
||||
@@ -166,18 +167,18 @@ func (st *HTTPStats) toServerHTTPStats() ServerHTTPStats {
|
||||
}
|
||||
|
||||
// Update statistics from http request and response data
|
||||
func (st *HTTPStats) updateStats(api string, r *http.Request, w *recordAPIStats, durationSecs float64) {
|
||||
func (st *HTTPStats) updateStats(api string, r *http.Request, w *logger.ResponseWriter, durationSecs float64) {
|
||||
// A successful request has a 2xx response code
|
||||
successReq := (w.respStatusCode >= 200 && w.respStatusCode < 300)
|
||||
successReq := (w.StatusCode >= 200 && w.StatusCode < 300)
|
||||
|
||||
if w.isS3Request && !strings.HasSuffix(r.URL.Path, prometheusMetricsPath) {
|
||||
if !strings.HasSuffix(r.URL.Path, prometheusMetricsPath) {
|
||||
st.totalS3Requests.Inc(api)
|
||||
if !successReq && w.respStatusCode != 0 {
|
||||
if !successReq && w.StatusCode != 0 {
|
||||
st.totalS3Errors.Inc(api)
|
||||
}
|
||||
}
|
||||
|
||||
if w.isS3Request && r.Method == "GET" {
|
||||
if r.Method == http.MethodGet {
|
||||
// Increment the prometheus http request response histogram with appropriate label
|
||||
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(durationSecs)
|
||||
}
|
||||
|
||||
+2
-1
@@ -123,7 +123,8 @@ func Trace(f http.HandlerFunc, logBody bool, w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
rw := logger.NewResponseWriter(w)
|
||||
rw.LogBody = logBody
|
||||
rw.LogErrBody = true
|
||||
rw.LogAllBody = logBody
|
||||
f(rw, r)
|
||||
|
||||
rq := trace.RequestInfo{
|
||||
|
||||
@@ -1,91 +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 cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// records the incoming bytes from the underlying request.Body.
|
||||
type recordTrafficRequest struct {
|
||||
io.ReadCloser
|
||||
isS3Request bool
|
||||
}
|
||||
|
||||
// Records the bytes read.
|
||||
func (r *recordTrafficRequest) Read(p []byte) (n int, err error) {
|
||||
n, err = r.ReadCloser.Read(p)
|
||||
globalConnStats.incInputBytes(n)
|
||||
if r.isS3Request {
|
||||
globalConnStats.incS3InputBytes(n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Records the outgoing bytes through the responseWriter.
|
||||
type recordTrafficResponse struct {
|
||||
// wrapper for underlying http.ResponseWriter.
|
||||
http.ResponseWriter
|
||||
isS3Request bool
|
||||
}
|
||||
|
||||
// Records the output bytes
|
||||
func (r *recordTrafficResponse) Write(p []byte) (n int, err error) {
|
||||
n, err = r.ResponseWriter.Write(p)
|
||||
globalConnStats.incOutputBytes(n)
|
||||
// Check if it is s3 request
|
||||
if r.isS3Request {
|
||||
globalConnStats.incS3OutputBytes(n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Calls the underlying Flush.
|
||||
func (r *recordTrafficResponse) Flush() {
|
||||
r.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// Records the outgoing bytes through the responseWriter.
|
||||
type recordAPIStats struct {
|
||||
http.ResponseWriter
|
||||
TTFB time.Time // TimeToFirstByte.
|
||||
firstByteRead bool
|
||||
respStatusCode int
|
||||
isS3Request bool
|
||||
}
|
||||
|
||||
// Calls the underlying WriteHeader.
|
||||
func (r *recordAPIStats) WriteHeader(i int) {
|
||||
r.respStatusCode = i
|
||||
r.ResponseWriter.WriteHeader(i)
|
||||
}
|
||||
|
||||
// Records the TTFB on the first byte write.
|
||||
func (r *recordAPIStats) Write(p []byte) (n int, err error) {
|
||||
if !r.firstByteRead {
|
||||
r.TTFB = UTCNow()
|
||||
r.firstByteRead = true
|
||||
}
|
||||
return r.ResponseWriter.Write(p)
|
||||
}
|
||||
|
||||
// Calls the underlying Flush.
|
||||
func (r *recordAPIStats) Flush() {
|
||||
r.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.
|
||||
* 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 stats
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// IncomingTrafficMeter counts the incoming bytes from the underlying request.Body.
|
||||
type IncomingTrafficMeter struct {
|
||||
io.ReadCloser
|
||||
countBytes int
|
||||
}
|
||||
|
||||
// Read calls the underlying Read and counts the transferred bytes.
|
||||
func (r *IncomingTrafficMeter) Read(p []byte) (n int, err error) {
|
||||
n, err = r.ReadCloser.Read(p)
|
||||
r.countBytes += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
// BytesCount returns the number of transferred bytes
|
||||
func (r IncomingTrafficMeter) BytesCount() int {
|
||||
return r.countBytes
|
||||
}
|
||||
|
||||
// OutgoingTrafficMeter counts the outgoing bytes through the responseWriter.
|
||||
type OutgoingTrafficMeter struct {
|
||||
// wrapper for underlying http.ResponseWriter.
|
||||
http.ResponseWriter
|
||||
countBytes int
|
||||
}
|
||||
|
||||
// Write calls the underlying write and counts the output bytes
|
||||
func (w *OutgoingTrafficMeter) Write(p []byte) (n int, err error) {
|
||||
n, err = w.ResponseWriter.Write(p)
|
||||
w.countBytes += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Flush calls the underlying Flush.
|
||||
func (w *OutgoingTrafficMeter) Flush() {
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// BytesCount returns the number of transferred bytes
|
||||
func (w OutgoingTrafficMeter) BytesCount() int {
|
||||
return w.countBytes
|
||||
}
|
||||
+124
-32
@@ -145,8 +145,8 @@ type UserIdentity struct {
|
||||
Credentials auth.Credentials `json:"credentials"`
|
||||
}
|
||||
|
||||
func newUserIdentity(creds auth.Credentials) UserIdentity {
|
||||
return UserIdentity{Version: 1, Credentials: creds}
|
||||
func newUserIdentity(cred auth.Credentials) UserIdentity {
|
||||
return UserIdentity{Version: 1, Credentials: cred}
|
||||
}
|
||||
|
||||
// GroupInfo contains info about a group
|
||||
@@ -354,6 +354,25 @@ func (sys *IAMSys) LoadUser(objAPI ObjectLayer, accessKey string, userType IAMUs
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadServiceAccount - reloads a specific service account from backend disks or etcd.
|
||||
func (sys *IAMSys) LoadServiceAccount(accessKey string) error {
|
||||
if sys == nil || sys.store == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.store.lock()
|
||||
defer sys.store.unlock()
|
||||
|
||||
if globalEtcdClient == nil {
|
||||
err := sys.store.loadUser(accessKey, srvAccUser, sys.iamUsersMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// When etcd is set, we use watch APIs so this code is not needed.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform IAM configuration migration.
|
||||
func (sys *IAMSys) doIAMConfigMigration(ctx context.Context) error {
|
||||
return sys.store.migrateBackendFormat(ctx)
|
||||
@@ -421,7 +440,7 @@ func (sys *IAMSys) DeletePolicy(policyName string) error {
|
||||
// This case cannot happen
|
||||
return errNoSuchUser
|
||||
}
|
||||
// User is from STS if the creds are temporary
|
||||
// User is from STS if the cred are temporary
|
||||
if cr.IsTemp() {
|
||||
usersType = append(usersType, stsUser)
|
||||
} else {
|
||||
@@ -534,6 +553,16 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
// Delete any service accounts if any first.
|
||||
for _, u := range sys.iamUsersMap {
|
||||
if u.IsServiceAccount() {
|
||||
if u.ParentUser == accessKey {
|
||||
_ = sys.store.deleteUserIdentity(u.AccessKey, srvAccUser)
|
||||
delete(sys.iamUsersMap, u.AccessKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It is ok to ignore deletion error on the mapped policy
|
||||
sys.store.deleteMappedPolicy(accessKey, regularUser, false)
|
||||
err := sys.store.deleteUserIdentity(accessKey, regularUser)
|
||||
@@ -545,15 +574,6 @@ func (sys *IAMSys) DeleteUser(accessKey string) error {
|
||||
delete(sys.iamUsersMap, accessKey)
|
||||
delete(sys.iamUserPolicyMap, accessKey)
|
||||
|
||||
for _, u := range sys.iamUsersMap {
|
||||
if u.IsServiceAccount() {
|
||||
if u.ParentUser == accessKey {
|
||||
_ = sys.store.deleteUserIdentity(u.AccessKey, srvAccUser)
|
||||
delete(sys.iamUsersMap, u.AccessKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -640,12 +660,12 @@ func (sys *IAMSys) IsTempUser(name string) (bool, error) {
|
||||
sys.store.rlock()
|
||||
defer sys.store.runlock()
|
||||
|
||||
creds, found := sys.iamUsersMap[name]
|
||||
cred, found := sys.iamUsersMap[name]
|
||||
if !found {
|
||||
return false, errNoSuchUser
|
||||
}
|
||||
|
||||
return creds.IsTemp(), nil
|
||||
return cred.IsTemp(), nil
|
||||
}
|
||||
|
||||
// IsServiceAccount - returns if given key is a service account
|
||||
@@ -658,13 +678,13 @@ func (sys *IAMSys) IsServiceAccount(name string) (bool, string, error) {
|
||||
sys.store.rlock()
|
||||
defer sys.store.runlock()
|
||||
|
||||
creds, found := sys.iamUsersMap[name]
|
||||
cred, found := sys.iamUsersMap[name]
|
||||
if !found {
|
||||
return false, "", errNoSuchUser
|
||||
}
|
||||
|
||||
if creds.IsServiceAccount() {
|
||||
return true, creds.ParentUser, nil
|
||||
if cred.IsServiceAccount() {
|
||||
return true, cred.ParentUser, nil
|
||||
}
|
||||
|
||||
return false, "", nil
|
||||
@@ -694,19 +714,19 @@ func (sys *IAMSys) GetUserInfo(name string) (u madmin.UserInfo, err error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
creds, found := sys.iamUsersMap[name]
|
||||
cred, found := sys.iamUsersMap[name]
|
||||
if !found {
|
||||
return u, errNoSuchUser
|
||||
}
|
||||
|
||||
if creds.IsTemp() {
|
||||
if cred.IsTemp() || cred.IsServiceAccount() {
|
||||
return u, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
u = madmin.UserInfo{
|
||||
PolicyName: sys.iamUserPolicyMap[name].Policy,
|
||||
Status: func() madmin.AccountStatus {
|
||||
if creds.IsValid() {
|
||||
if cred.IsValid() {
|
||||
return madmin.AccountEnabled
|
||||
}
|
||||
return madmin.AccountDisabled
|
||||
@@ -739,7 +759,7 @@ func (sys *IAMSys) SetUserStatus(accessKey string, status madmin.AccountStatus)
|
||||
return errNoSuchUser
|
||||
}
|
||||
|
||||
if cred.IsTemp() {
|
||||
if cred.IsTemp() || cred.IsServiceAccount() {
|
||||
return errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
@@ -787,10 +807,6 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, ses
|
||||
sys.store.lock()
|
||||
defer sys.store.unlock()
|
||||
|
||||
if sys.usersSysType != MinIOUsersSysType {
|
||||
return auth.Credentials{}, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
if parentUser == globalActiveCred.AccessKey {
|
||||
return auth.Credentials{}, errIAMActionNotAllowed
|
||||
}
|
||||
@@ -800,7 +816,16 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, ses
|
||||
return auth.Credentials{}, errNoSuchUser
|
||||
}
|
||||
|
||||
if cr.IsTemp() {
|
||||
// Disallow service accounts to further create more service accounts.
|
||||
if cr.IsServiceAccount() {
|
||||
return auth.Credentials{}, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
// FIXME: Disallow temporary users with no parent, this is most
|
||||
// probably with OpenID which we don't support to provide
|
||||
// any parent user, LDAPUsersType and MinIOUsersType are
|
||||
// currently supported.
|
||||
if cr.ParentUser == "" && cr.IsTemp() {
|
||||
return auth.Credentials{}, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
@@ -819,8 +844,8 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, ses
|
||||
if err != nil {
|
||||
return auth.Credentials{}, err
|
||||
}
|
||||
|
||||
cred.ParentUser = parentUser
|
||||
|
||||
u := newUserIdentity(cred)
|
||||
|
||||
if err := sys.store.saveUserIdentity(u.Credentials.AccessKey, srvAccUser, u); err != nil {
|
||||
@@ -832,6 +857,73 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, ses
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
// ListServiceAccounts - lists all services accounts associated to a specific user
|
||||
func (sys *IAMSys) ListServiceAccounts(ctx context.Context, accessKey string) ([]string, error) {
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || sys == nil || sys.store == nil {
|
||||
return nil, errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.store.rlock()
|
||||
defer sys.store.runlock()
|
||||
|
||||
var serviceAccounts []string
|
||||
|
||||
for k, v := range sys.iamUsersMap {
|
||||
if v.IsServiceAccount() && v.ParentUser == accessKey {
|
||||
serviceAccounts = append(serviceAccounts, k)
|
||||
}
|
||||
}
|
||||
|
||||
return serviceAccounts, nil
|
||||
}
|
||||
|
||||
// GetServiceAccountParent - gets information about a service account
|
||||
func (sys *IAMSys) GetServiceAccountParent(ctx context.Context, accessKey string) (string, error) {
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || sys == nil || sys.store == nil {
|
||||
return "", errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.store.rlock()
|
||||
defer sys.store.runlock()
|
||||
|
||||
sa, ok := sys.iamUsersMap[accessKey]
|
||||
if ok && sa.IsServiceAccount() {
|
||||
return sa.ParentUser, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// DeleteServiceAccount - delete a service account
|
||||
func (sys *IAMSys) DeleteServiceAccount(ctx context.Context, accessKey string) error {
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objectAPI == nil || sys == nil || sys.store == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
sys.store.lock()
|
||||
defer sys.store.unlock()
|
||||
|
||||
sa, ok := sys.iamUsersMap[accessKey]
|
||||
if !ok || !sa.IsServiceAccount() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// It is ok to ignore deletion error on the mapped policy
|
||||
err := sys.store.deleteUserIdentity(accessKey, srvAccUser)
|
||||
if err != nil {
|
||||
// ignore if user is already deleted.
|
||||
if err == errNoSuchUser {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
delete(sys.iamUsersMap, accessKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetUser - set user credentials and policy.
|
||||
func (sys *IAMSys) SetUser(accessKey string, uinfo madmin.UserInfo) error {
|
||||
objectAPI := newObjectLayerWithoutSafeModeFn()
|
||||
@@ -910,6 +1002,11 @@ func (sys *IAMSys) GetUser(accessKey string) (cred auth.Credentials, ok bool) {
|
||||
defer sys.store.runlock()
|
||||
|
||||
cred, ok = sys.iamUsersMap[accessKey]
|
||||
if ok && cred.IsValid() {
|
||||
if cred.ParentUser != "" {
|
||||
_, ok = sys.iamUsersMap[cred.ParentUser]
|
||||
}
|
||||
}
|
||||
return cred, ok && cred.IsValid()
|
||||
}
|
||||
|
||||
@@ -1263,7 +1360,6 @@ var iamAccountOtherAccessActions = iampolicy.NewActionSet(
|
||||
func (sys *IAMSys) GetAccountAccess(accountName, bucket string) (rd, wr, o bool) {
|
||||
policies, err := sys.PolicyDBGet(accountName, false)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false, false, false
|
||||
}
|
||||
|
||||
@@ -1410,7 +1506,6 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parent string) b
|
||||
// Check if the parent is allowed to perform this action, reject if not
|
||||
parentUserPolicies, err := sys.PolicyDBGet(parent, false)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1631,7 +1726,6 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
// If the credential is temporary, perform STS related checks.
|
||||
ok, err := sys.IsTempUser(args.AccountName)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
}
|
||||
if ok {
|
||||
@@ -1641,7 +1735,6 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
// If the credential is for a service account, perform related check
|
||||
ok, parentUser, err := sys.IsServiceAccount(args.AccountName)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
}
|
||||
if ok {
|
||||
@@ -1651,7 +1744,6 @@ func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
// Continue with the assumption of a regular user
|
||||
policies, err := sys.PolicyDBGet(args.AccountName, false)
|
||||
if err != nil {
|
||||
logger.LogIf(context.Background(), err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+31
-19
@@ -32,8 +32,11 @@ import (
|
||||
type ResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
StatusCode int
|
||||
// Response body should be logged
|
||||
LogBody bool
|
||||
// Log body of 4xx or 5xx responses
|
||||
LogErrBody bool
|
||||
// Log body of all responses
|
||||
LogAllBody bool
|
||||
|
||||
TimeToFirstByte time.Duration
|
||||
StartTime time.Time
|
||||
// number of bytes written
|
||||
@@ -67,7 +70,7 @@ func (lrw *ResponseWriter) Write(p []byte) (int, error) {
|
||||
lrw.writeHeaders(&lrw.headers, http.StatusOK, lrw.Header())
|
||||
lrw.headersLogged = true
|
||||
}
|
||||
if lrw.StatusCode >= http.StatusBadRequest || lrw.LogBody {
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
// Always logging error responses.
|
||||
lrw.body.Write(p)
|
||||
}
|
||||
@@ -94,7 +97,7 @@ var BodyPlaceHolder = []byte("<BODY>")
|
||||
func (lrw *ResponseWriter) Body() []byte {
|
||||
// If there was an error response or body logging is enabled
|
||||
// then we return the body contents
|
||||
if lrw.StatusCode >= http.StatusBadRequest || lrw.LogBody {
|
||||
if (lrw.LogErrBody && lrw.StatusCode >= http.StatusBadRequest) || lrw.LogAllBody {
|
||||
return lrw.body.Bytes()
|
||||
}
|
||||
// ... otherwise we return the <BODY> place holder
|
||||
@@ -132,14 +135,22 @@ func AddAuditTarget(t Target) {
|
||||
|
||||
// AuditLog - logs audit logs to all audit targets.
|
||||
func AuditLog(w http.ResponseWriter, r *http.Request, api string, reqClaims map[string]interface{}) {
|
||||
var statusCode int
|
||||
var timeToResponse time.Duration
|
||||
var timeToFirstByte time.Duration
|
||||
lrw, ok := w.(*ResponseWriter)
|
||||
// Fast exit if there is not audit target configured
|
||||
if len(AuditTargets) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
statusCode int
|
||||
timeToResponse time.Duration
|
||||
timeToFirstByte time.Duration
|
||||
)
|
||||
|
||||
st, ok := w.(*ResponseWriter)
|
||||
if ok {
|
||||
statusCode = lrw.StatusCode
|
||||
timeToResponse = time.Now().UTC().Sub(lrw.StartTime)
|
||||
timeToFirstByte = lrw.TimeToFirstByte
|
||||
statusCode = st.StatusCode
|
||||
timeToResponse = time.Now().UTC().Sub(st.StartTime)
|
||||
timeToFirstByte = st.TimeToFirstByte
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
@@ -149,16 +160,17 @@ func AuditLog(w http.ResponseWriter, r *http.Request, api string, reqClaims map[
|
||||
object = vars["object"]
|
||||
}
|
||||
|
||||
entry := audit.ToEntry(w, r, reqClaims, globalDeploymentID)
|
||||
entry.API.Name = api
|
||||
entry.API.Bucket = bucket
|
||||
entry.API.Object = object
|
||||
entry.API.Status = http.StatusText(statusCode)
|
||||
entry.API.StatusCode = statusCode
|
||||
entry.API.TimeToFirstByte = timeToFirstByte.String()
|
||||
entry.API.TimeToResponse = timeToResponse.String()
|
||||
|
||||
// Send audit logs only to http targets.
|
||||
for _, t := range AuditTargets {
|
||||
entry := audit.ToEntry(w, r, reqClaims, globalDeploymentID)
|
||||
entry.API.Name = api
|
||||
entry.API.Bucket = bucket
|
||||
entry.API.Object = object
|
||||
entry.API.Status = http.StatusText(statusCode)
|
||||
entry.API.StatusCode = statusCode
|
||||
entry.API.TimeToFirstByte = timeToFirstByte.String()
|
||||
entry.API.TimeToResponse = timeToResponse.String()
|
||||
_ = t.Send(entry, string(All))
|
||||
}
|
||||
}
|
||||
|
||||
+152
-94
@@ -47,9 +47,11 @@ const (
|
||||
Endpoint = "endpoint"
|
||||
AuthToken = "auth_token"
|
||||
|
||||
EnvLoggerWebhookEnable = "MINIO_LOGGER_WEBHOOK_ENABLE"
|
||||
EnvLoggerWebhookEndpoint = "MINIO_LOGGER_WEBHOOK_ENDPOINT"
|
||||
EnvLoggerWebhookAuthToken = "MINIO_LOGGER_WEBHOOK_AUTH_TOKEN"
|
||||
|
||||
EnvAuditWebhookEnable = "MINIO_AUDIT_WEBHOOK_ENABLE"
|
||||
EnvAuditWebhookEndpoint = "MINIO_AUDIT_WEBHOOK_ENDPOINT"
|
||||
EnvAuditWebhookAuthToken = "MINIO_AUDIT_WEBHOOK_AUTH_TOKEN"
|
||||
)
|
||||
@@ -110,9 +112,72 @@ func NewConfig() Config {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func lookupLegacyConfig() (Config, error) {
|
||||
cfg := NewConfig()
|
||||
|
||||
var loggerTargets []string
|
||||
envs := env.List(legacyEnvLoggerHTTPEndpoint)
|
||||
for _, k := range envs {
|
||||
target := strings.TrimPrefix(k, legacyEnvLoggerHTTPEndpoint+config.Default)
|
||||
if target == legacyEnvLoggerHTTPEndpoint {
|
||||
target = config.Default
|
||||
}
|
||||
loggerTargets = append(loggerTargets, target)
|
||||
}
|
||||
|
||||
// Load HTTP logger from the environment if found
|
||||
for _, target := range loggerTargets {
|
||||
endpointEnv := legacyEnvLoggerHTTPEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = legacyEnvLoggerHTTPEndpoint + config.Default + target
|
||||
}
|
||||
endpoint := env.Get(endpointEnv, "")
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
cfg.HTTP[target] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// List legacy audit ENVs if any.
|
||||
var loggerAuditTargets []string
|
||||
envs = env.List(legacyEnvAuditLoggerHTTPEndpoint)
|
||||
for _, k := range envs {
|
||||
target := strings.TrimPrefix(k, legacyEnvAuditLoggerHTTPEndpoint+config.Default)
|
||||
if target == legacyEnvAuditLoggerHTTPEndpoint {
|
||||
target = config.Default
|
||||
}
|
||||
loggerAuditTargets = append(loggerAuditTargets, target)
|
||||
}
|
||||
|
||||
for _, target := range loggerAuditTargets {
|
||||
endpointEnv := legacyEnvAuditLoggerHTTPEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = legacyEnvAuditLoggerHTTPEndpoint + config.Default + target
|
||||
}
|
||||
endpoint := env.Get(endpointEnv, "")
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
cfg.Audit[target] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
|
||||
}
|
||||
|
||||
// LookupConfig - lookup logger config, override with ENVs if set.
|
||||
func LookupConfig(scfg config.Config) (Config, error) {
|
||||
cfg := NewConfig()
|
||||
// Lookup for legacy environment variables first
|
||||
cfg, err := lookupLegacyConfig()
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
envs := env.List(EnvLoggerWebhookEndpoint)
|
||||
var loggerTargets []string
|
||||
@@ -134,89 +199,21 @@ func LookupConfig(scfg config.Config) (Config, error) {
|
||||
loggerAuditTargets = append(loggerAuditTargets, target)
|
||||
}
|
||||
|
||||
// List legacy ENVs if any.
|
||||
envs = env.List(EnvAuditLoggerHTTPEndpoint)
|
||||
for _, k := range envs {
|
||||
target := strings.TrimPrefix(k, EnvAuditLoggerHTTPEndpoint+config.Default)
|
||||
if target == EnvAuditLoggerHTTPEndpoint {
|
||||
target = config.Default
|
||||
}
|
||||
loggerAuditTargets = append(loggerAuditTargets, target)
|
||||
}
|
||||
|
||||
for starget, kv := range scfg[config.LoggerWebhookSubSys] {
|
||||
subSysTarget := config.LoggerWebhookSubSys
|
||||
if starget != config.Default {
|
||||
subSysTarget = config.LoggerWebhookSubSys + config.SubSystemSeparator + starget
|
||||
}
|
||||
if err := config.CheckValidKeys(subSysTarget, kv, DefaultKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
enabled, err := config.ParseBool(kv.Get(config.Enable))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
endpointEnv := EnvLoggerWebhookEndpoint
|
||||
if starget != config.Default {
|
||||
endpointEnv = EnvLoggerWebhookEndpoint + config.Default + starget
|
||||
}
|
||||
authTokenEnv := EnvLoggerWebhookAuthToken
|
||||
if starget != config.Default {
|
||||
authTokenEnv = EnvLoggerWebhookAuthToken + config.Default + starget
|
||||
}
|
||||
cfg.HTTP[starget] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: env.Get(endpointEnv, kv.Get(Endpoint)),
|
||||
AuthToken: env.Get(authTokenEnv, kv.Get(AuthToken)),
|
||||
}
|
||||
}
|
||||
|
||||
for starget, kv := range scfg[config.AuditWebhookSubSys] {
|
||||
subSysTarget := config.AuditWebhookSubSys
|
||||
if starget != config.Default {
|
||||
subSysTarget = config.AuditWebhookSubSys + config.SubSystemSeparator + starget
|
||||
}
|
||||
if err := config.CheckValidKeys(subSysTarget, kv, DefaultAuditKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
enabled, err := config.ParseBool(kv.Get(config.Enable))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
endpointEnv := EnvAuditWebhookEndpoint
|
||||
if starget != config.Default {
|
||||
endpointEnv = EnvAuditWebhookEndpoint + config.Default + starget
|
||||
}
|
||||
legacyEndpointEnv := EnvAuditLoggerHTTPEndpoint
|
||||
if starget != config.Default {
|
||||
legacyEndpointEnv = EnvAuditLoggerHTTPEndpoint + config.Default + starget
|
||||
}
|
||||
endpoint := env.Get(legacyEndpointEnv, "")
|
||||
if endpoint == "" {
|
||||
endpoint = env.Get(endpointEnv, kv.Get(Endpoint))
|
||||
}
|
||||
authTokenEnv := EnvAuditWebhookAuthToken
|
||||
if starget != config.Default {
|
||||
authTokenEnv = EnvAuditWebhookAuthToken + config.Default + starget
|
||||
}
|
||||
cfg.Audit[starget] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: endpoint,
|
||||
AuthToken: env.Get(authTokenEnv, kv.Get(AuthToken)),
|
||||
}
|
||||
}
|
||||
|
||||
// Load HTTP logger from the environment if found
|
||||
for _, target := range loggerTargets {
|
||||
if v, ok := cfg.HTTP[target]; ok && v.Enabled {
|
||||
// This target is already enabled using the
|
||||
// legacy environment variables, ignore.
|
||||
continue
|
||||
}
|
||||
enableEnv := EnvLoggerWebhookEnable
|
||||
if target != config.Default {
|
||||
enableEnv = EnvLoggerWebhookEnable + config.Default + target
|
||||
}
|
||||
enable, err := config.ParseBool(env.Get(enableEnv, ""))
|
||||
if err != nil || !enable {
|
||||
continue
|
||||
}
|
||||
endpointEnv := EnvLoggerWebhookEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = EnvLoggerWebhookEndpoint + config.Default + target
|
||||
@@ -233,28 +230,89 @@ func LookupConfig(scfg config.Config) (Config, error) {
|
||||
}
|
||||
|
||||
for _, target := range loggerAuditTargets {
|
||||
endpointEnv := EnvLoggerWebhookEndpoint
|
||||
if target != config.Default {
|
||||
endpointEnv = EnvLoggerWebhookEndpoint + config.Default + target
|
||||
if v, ok := cfg.Audit[target]; ok && v.Enabled {
|
||||
// This target is already enabled using the
|
||||
// legacy environment variables, ignore.
|
||||
continue
|
||||
}
|
||||
legacyEndpointEnv := EnvAuditLoggerHTTPEndpoint
|
||||
enableEnv := EnvAuditWebhookEnable
|
||||
if target != config.Default {
|
||||
legacyEndpointEnv = EnvAuditLoggerHTTPEndpoint + config.Default + target
|
||||
enableEnv = EnvAuditWebhookEnable + config.Default + target
|
||||
}
|
||||
endpoint := env.Get(legacyEndpointEnv, "")
|
||||
if endpoint == "" {
|
||||
endpoint = env.Get(endpointEnv, "")
|
||||
enable, err := config.ParseBool(env.Get(enableEnv, ""))
|
||||
if err != nil || !enable {
|
||||
continue
|
||||
}
|
||||
authTokenEnv := EnvLoggerWebhookAuthToken
|
||||
endpointEnv := EnvAuditWebhookEndpoint
|
||||
if target != config.Default {
|
||||
authTokenEnv = EnvLoggerWebhookAuthToken + config.Default + target
|
||||
endpointEnv = EnvAuditWebhookEndpoint + config.Default + target
|
||||
}
|
||||
authTokenEnv := EnvAuditWebhookAuthToken
|
||||
if target != config.Default {
|
||||
authTokenEnv = EnvAuditWebhookAuthToken + config.Default + target
|
||||
}
|
||||
cfg.Audit[target] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: endpoint,
|
||||
Endpoint: env.Get(endpointEnv, ""),
|
||||
AuthToken: env.Get(authTokenEnv, ""),
|
||||
}
|
||||
}
|
||||
|
||||
for starget, kv := range scfg[config.LoggerWebhookSubSys] {
|
||||
if l, ok := cfg.HTTP[starget]; ok && l.Enabled {
|
||||
// Ignore this HTTP logger config since there is
|
||||
// a target with the same name loaded and enabled
|
||||
// from the environment.
|
||||
continue
|
||||
}
|
||||
subSysTarget := config.LoggerWebhookSubSys
|
||||
if starget != config.Default {
|
||||
subSysTarget = config.LoggerWebhookSubSys + config.SubSystemSeparator + starget
|
||||
}
|
||||
if err := config.CheckValidKeys(subSysTarget, kv, DefaultKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
enabled, err := config.ParseBool(kv.Get(config.Enable))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
cfg.HTTP[starget] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: kv.Get(Endpoint),
|
||||
AuthToken: kv.Get(AuthToken),
|
||||
}
|
||||
}
|
||||
|
||||
for starget, kv := range scfg[config.AuditWebhookSubSys] {
|
||||
if l, ok := cfg.Audit[starget]; ok && l.Enabled {
|
||||
// Ignore this audit config since another target
|
||||
// with the same name is already loaded and enabled
|
||||
// in the shell environment.
|
||||
continue
|
||||
}
|
||||
subSysTarget := config.AuditWebhookSubSys
|
||||
if starget != config.Default {
|
||||
subSysTarget = config.AuditWebhookSubSys + config.SubSystemSeparator + starget
|
||||
}
|
||||
if err := config.CheckValidKeys(subSysTarget, kv, DefaultAuditKVS); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
enabled, err := config.ParseBool(kv.Get(config.Enable))
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
cfg.Audit[starget] = HTTP{
|
||||
Enabled: true,
|
||||
Endpoint: kv.Get(Endpoint),
|
||||
AuthToken: kv.Get(AuthToken),
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import "github.com/minio/minio/cmd/config"
|
||||
|
||||
// Legacy envs
|
||||
const (
|
||||
EnvAuditLoggerHTTPEndpoint = "MINIO_AUDIT_LOGGER_HTTP_ENDPOINT"
|
||||
legacyEnvAuditLoggerHTTPEndpoint = "MINIO_AUDIT_LOGGER_HTTP_ENDPOINT"
|
||||
legacyEnvLoggerHTTPEndpoint = "MINIO_LOGGER_HTTP_ENDPOINT"
|
||||
)
|
||||
|
||||
// SetLoggerHTTPAudit - helper for migrating older config to newer KV format.
|
||||
|
||||
+9
-10
@@ -39,9 +39,9 @@ type mergeWalk struct {
|
||||
// mergeWalkPool's purpose is to maintain active mergeWalk go-routines in a map so that
|
||||
// it can be looked up across related list calls.
|
||||
type MergeWalkPool struct {
|
||||
sync.Mutex
|
||||
pool map[listParams][]mergeWalk
|
||||
timeOut time.Duration
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewMergeWalkPool - initialize new tree walk pool.
|
||||
@@ -49,7 +49,6 @@ func NewMergeWalkPool(timeout time.Duration) *MergeWalkPool {
|
||||
tPool := &MergeWalkPool{
|
||||
pool: make(map[listParams][]mergeWalk),
|
||||
timeOut: timeout,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
return tPool
|
||||
}
|
||||
@@ -58,9 +57,9 @@ func NewMergeWalkPool(timeout time.Duration) *MergeWalkPool {
|
||||
// listParams, removes it from the pool, and returns the MergeWalkResult
|
||||
// channel.
|
||||
// Returns nil if listParams does not have an asccociated mergeWalk.
|
||||
func (t MergeWalkPool) Release(params listParams) ([]FileInfoCh, chan struct{}) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
func (t *MergeWalkPool) Release(params listParams) ([]FileInfoCh, chan struct{}) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
walks, ok := t.pool[params] // Pick the valid walks.
|
||||
if ok {
|
||||
if len(walks) > 0 {
|
||||
@@ -88,9 +87,9 @@ func (t MergeWalkPool) Release(params listParams) ([]FileInfoCh, chan struct{})
|
||||
// 2) Relase() signals the timer go-routine to end on endTimerCh.
|
||||
// During listing the timer should not timeout and end the mergeWalk go-routine, hence the
|
||||
// timer go-routine should be ended.
|
||||
func (t MergeWalkPool) Set(params listParams, resultChs []FileInfoCh, endWalkCh chan struct{}) {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
func (t *MergeWalkPool) Set(params listParams, resultChs []FileInfoCh, endWalkCh chan struct{}) {
|
||||
t.Lock()
|
||||
defer t.Unlock()
|
||||
|
||||
// Should be a buffered channel so that Release() never blocks.
|
||||
endTimerCh := make(chan struct{}, 1)
|
||||
@@ -111,7 +110,7 @@ func (t MergeWalkPool) Set(params listParams, resultChs []FileInfoCh, endWalkCh
|
||||
case <-time.After(t.timeOut):
|
||||
// Timeout has expired. Remove the mergeWalk from mergeWalkPool and
|
||||
// end the mergeWalk go-routine.
|
||||
t.lock.Lock()
|
||||
t.Lock()
|
||||
walks, ok := t.pool[params]
|
||||
if ok {
|
||||
// Trick of filtering without allocating
|
||||
@@ -135,7 +134,7 @@ func (t MergeWalkPool) Set(params listParams, resultChs []FileInfoCh, endWalkCh
|
||||
}
|
||||
// Signal the mergeWalk go-routine to die.
|
||||
close(endWalkCh)
|
||||
t.lock.Unlock()
|
||||
t.Unlock()
|
||||
case <-endTimerCh:
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,22 +80,22 @@ func TestManyMergeWalksSameParam(t *testing.T) {
|
||||
tw.Set(params, walkChs, endWalkCh)
|
||||
}
|
||||
|
||||
tw.lock.Lock()
|
||||
tw.Lock()
|
||||
if walks, ok := tw.pool[params]; ok {
|
||||
if len(walks) != 10 {
|
||||
t.Error("There aren't as many walks as were Set")
|
||||
}
|
||||
}
|
||||
tw.lock.Unlock()
|
||||
tw.Unlock()
|
||||
for i := 0; i < 10; i++ {
|
||||
tw.lock.Lock()
|
||||
tw.Lock()
|
||||
if walks, ok := tw.pool[params]; ok {
|
||||
// Before ith Release we should have 10-i treeWalk go-routines.
|
||||
if 10-i != len(walks) {
|
||||
t.Error("There aren't as many walks as were Set")
|
||||
}
|
||||
}
|
||||
tw.lock.Unlock()
|
||||
tw.Unlock()
|
||||
tw.Release(params)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -19,6 +19,7 @@ package cmd
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
@@ -190,7 +191,7 @@ func gatewayMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
"Total number of requests made to "+globalGatewayName+" by current MinIO Gateway",
|
||||
[]string{"method"}, nil),
|
||||
prometheus.CounterValue,
|
||||
float64(s.Get.Load()),
|
||||
float64(atomic.LoadUint64(&s.Get)),
|
||||
http.MethodGet,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
@@ -199,7 +200,7 @@ func gatewayMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
"Total number of requests made to "+globalGatewayName+" by current MinIO Gateway",
|
||||
[]string{"method"}, nil),
|
||||
prometheus.CounterValue,
|
||||
float64(s.Head.Load()),
|
||||
float64(atomic.LoadUint64(&s.Head)),
|
||||
http.MethodHead,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
@@ -208,7 +209,7 @@ func gatewayMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
"Total number of requests made to "+globalGatewayName+" by current MinIO Gateway",
|
||||
[]string{"method"}, nil),
|
||||
prometheus.CounterValue,
|
||||
float64(s.Put.Load()),
|
||||
float64(atomic.LoadUint64(&s.Put)),
|
||||
http.MethodPut,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
@@ -217,7 +218,7 @@ func gatewayMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
"Total number of requests made to "+globalGatewayName+" by current MinIO Gateway",
|
||||
[]string{"method"}, nil),
|
||||
prometheus.CounterValue,
|
||||
float64(s.Post.Load()),
|
||||
float64(atomic.LoadUint64(&s.Post)),
|
||||
http.MethodPost,
|
||||
)
|
||||
}
|
||||
|
||||
+178
-83
@@ -19,10 +19,10 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
@@ -31,25 +31,25 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zip"
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/cmd/crypto"
|
||||
"github.com/minio/minio/cmd/logger"
|
||||
|
||||
bucketsse "github.com/minio/minio/pkg/bucket/encryption"
|
||||
"github.com/minio/minio/pkg/bucket/lifecycle"
|
||||
objectlock "github.com/minio/minio/pkg/bucket/object/lock"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/set"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
xnet "github.com/minio/minio/pkg/net"
|
||||
"github.com/minio/minio/pkg/sync/errgroup"
|
||||
"github.com/willf/bloom"
|
||||
)
|
||||
|
||||
// NotificationSys - notification system.
|
||||
type NotificationSys struct {
|
||||
sync.RWMutex
|
||||
targetList *event.TargetList
|
||||
targetResCh chan event.TargetIDResult
|
||||
bucketRulesMap map[string]event.RulesMap
|
||||
bucketRemoteTargetRulesMap map[string]map[event.TargetID]event.RulesMap
|
||||
peerClients []*peerRESTClient
|
||||
@@ -243,6 +243,36 @@ func (sys *NotificationSys) LoadGroup(group string) []NotificationPeerErr {
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// DeleteServiceAccount - deletes a specific service account across all peers
|
||||
func (sys *NotificationSys) DeleteServiceAccount(accessKey string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(GlobalContext, func() error {
|
||||
return client.DeleteServiceAccount(accessKey)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// LoadServiceAccount - reloads a specific service account across all peers
|
||||
func (sys *NotificationSys) LoadServiceAccount(accessKey string) []NotificationPeerErr {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(GlobalContext, func() error {
|
||||
return client.LoadServiceAccount(accessKey)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// BackgroundHealStatus - returns background heal status of all peers
|
||||
func (sys *NotificationSys) BackgroundHealStatus() []madmin.BgHealState {
|
||||
states := make([]madmin.BgHealState, len(sys.peerClients))
|
||||
@@ -405,6 +435,75 @@ func (sys *NotificationSys) SignalService(sig serviceSignal) []NotificationPeerE
|
||||
return ng.Wait()
|
||||
}
|
||||
|
||||
// updateBloomFilter will cycle all servers to the current index and
|
||||
// return a merged bloom filter if a complete one can be retrieved.
|
||||
func (sys *NotificationSys) updateBloomFilter(ctx context.Context, current uint64) (*bloomFilter, error) {
|
||||
var req = bloomFilterRequest{
|
||||
Current: current,
|
||||
Oldest: current - dataUsageUpdateDirCycles,
|
||||
}
|
||||
if current < dataUsageUpdateDirCycles {
|
||||
req.Oldest = 0
|
||||
}
|
||||
|
||||
// Load initial state from local...
|
||||
var bf *bloomFilter
|
||||
bfr, err := intDataUpdateTracker.cycleFilter(ctx, req.Oldest, req.Current)
|
||||
logger.LogIf(ctx, err)
|
||||
if err == nil && bfr.Complete {
|
||||
nbf := intDataUpdateTracker.newBloomFilter()
|
||||
bf = &nbf
|
||||
_, err = bf.ReadFrom(bytes.NewBuffer(bfr.Filter))
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
g := errgroup.WithNErrs(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
g.Go(func() error {
|
||||
serverBF, err := client.cycleServerBloomFilter(ctx, req)
|
||||
if false && intDataUpdateTracker.debug {
|
||||
b, _ := json.MarshalIndent(serverBF, "", " ")
|
||||
logger.Info("Disk %v, Bloom filter: %v", client.host.Name, string(b))
|
||||
}
|
||||
// Keep lock while checking result.
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if err != nil || !serverBF.Complete || bf == nil {
|
||||
logger.LogIf(ctx, err)
|
||||
bf = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
var tmp bloom.BloomFilter
|
||||
_, err = tmp.ReadFrom(bytes.NewBuffer(serverBF.Filter))
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
bf = nil
|
||||
return nil
|
||||
}
|
||||
if bf.BloomFilter == nil {
|
||||
bf.BloomFilter = &tmp
|
||||
} else {
|
||||
err = bf.Merge(&tmp)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
bf = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, idx)
|
||||
}
|
||||
g.Wait()
|
||||
return bf, nil
|
||||
}
|
||||
|
||||
// GetLocks - makes GetLocks RPC call on all peers.
|
||||
func (sys *NotificationSys) GetLocks(ctx context.Context) []*PeerLocks {
|
||||
locksResp := make([]*PeerLocks, len(sys.peerClients))
|
||||
@@ -468,6 +567,7 @@ func (sys *NotificationSys) SetBucketPolicy(ctx context.Context, bucketName stri
|
||||
func (sys *NotificationSys) DeleteBucket(ctx context.Context, bucketName string) {
|
||||
globalNotificationSys.RemoveNotification(bucketName)
|
||||
globalBucketObjectLockConfig.Remove(bucketName)
|
||||
globalBucketQuotaSys.Remove(bucketName)
|
||||
globalPolicySys.Remove(bucketName)
|
||||
globalLifecycleSys.Remove(bucketName)
|
||||
|
||||
@@ -520,6 +620,23 @@ func (sys *NotificationSys) RemoveBucketObjectLockConfig(ctx context.Context, bu
|
||||
}()
|
||||
}
|
||||
|
||||
// RemoveBucketQuotaConfig - calls RemoveBucketQuotaConfig RPC call on all peers.
|
||||
func (sys *NotificationSys) RemoveBucketQuotaConfig(ctx context.Context, bucketName string) {
|
||||
go func() {
|
||||
ng := WithNPeers(len(sys.peerClients))
|
||||
for idx, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
client := client
|
||||
ng.Go(ctx, func() error {
|
||||
return client.RemoveBucketQuotaConfig(bucketName)
|
||||
}, idx, *client.host)
|
||||
}
|
||||
ng.Wait()
|
||||
}()
|
||||
}
|
||||
|
||||
// SetBucketLifecycle - calls SetBucketLifecycle on all peers.
|
||||
func (sys *NotificationSys) SetBucketLifecycle(ctx context.Context, bucketName string,
|
||||
bucketLifecycle *lifecycle.Lifecycle) {
|
||||
@@ -632,19 +749,6 @@ func (sys *NotificationSys) AddRemoteTarget(bucketName string, target event.Targ
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteTargetExist - checks whether given target ID is a HTTP/PeerRPC client target or not.
|
||||
func (sys *NotificationSys) RemoteTargetExist(bucketName string, targetID event.TargetID) bool {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
targetMap, ok := sys.bucketRemoteTargetRulesMap[bucketName]
|
||||
if ok {
|
||||
_, ok = targetMap[targetID]
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// Loads notification policies for all buckets into NotificationSys.
|
||||
func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
for _, bucket := range buckets {
|
||||
@@ -667,7 +771,7 @@ func (sys *NotificationSys) load(buckets []BucketInfo, objAPI ObjectLayer) error
|
||||
// Init - initializes notification system from notification.xml and listener.json of all buckets.
|
||||
func (sys *NotificationSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error {
|
||||
if objAPI == nil {
|
||||
return errInvalidArgument
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
// In gateway mode, notifications are not supported.
|
||||
@@ -683,6 +787,17 @@ func (sys *NotificationSys) Init(buckets []BucketInfo, objAPI ObjectLayer) error
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
for res := range sys.targetResCh {
|
||||
if res.Err != nil {
|
||||
reqInfo := &logger.ReqInfo{}
|
||||
reqInfo.AppendTags("targetID", res.ID.Name)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogOnceIf(ctx, res.Err, res.ID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return sys.load(buckets, objAPI)
|
||||
}
|
||||
|
||||
@@ -729,7 +844,9 @@ func (sys *NotificationSys) ConfiguredTargetIDs() []event.TargetID {
|
||||
for _, rmap := range sys.bucketRulesMap {
|
||||
for _, rules := range rmap {
|
||||
for _, targetSet := range rules {
|
||||
targetIDs = append(targetIDs, targetSet.ToSlice()...)
|
||||
for id := range targetSet {
|
||||
targetIDs = append(targetIDs, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -750,73 +867,45 @@ func (sys *NotificationSys) RemoveNotification(bucketName string) {
|
||||
|
||||
delete(sys.bucketRulesMap, bucketName)
|
||||
|
||||
targetIDSet := event.NewTargetIDSet()
|
||||
for targetID := range sys.bucketRemoteTargetRulesMap[bucketName] {
|
||||
sys.targetList.Remove(targetID)
|
||||
targetIDSet[targetID] = struct{}{}
|
||||
delete(sys.bucketRemoteTargetRulesMap[bucketName], targetID)
|
||||
}
|
||||
sys.targetList.Remove(targetIDSet)
|
||||
|
||||
delete(sys.bucketRemoteTargetRulesMap, bucketName)
|
||||
}
|
||||
|
||||
// RemoveAllRemoteTargets - closes and removes all HTTP/PeerRPC client targets.
|
||||
// RemoveAllRemoteTargets - closes and removes all notification targets.
|
||||
func (sys *NotificationSys) RemoveAllRemoteTargets() {
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
for _, targetMap := range sys.bucketRemoteTargetRulesMap {
|
||||
for targetID := range targetMap {
|
||||
sys.targetList.Remove(targetID)
|
||||
targetIDSet := event.NewTargetIDSet()
|
||||
for k := range targetMap {
|
||||
targetIDSet[k] = struct{}{}
|
||||
}
|
||||
sys.targetList.Remove(targetIDSet)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveRemoteTarget - closes and removes target by target ID.
|
||||
func (sys *NotificationSys) RemoveRemoteTarget(bucketName string, targetID event.TargetID) {
|
||||
for terr := range sys.targetList.Remove(targetID) {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("targetID", terr.ID.Name)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogIf(ctx, terr.Err)
|
||||
}
|
||||
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
if _, ok := sys.bucketRemoteTargetRulesMap[bucketName]; ok {
|
||||
delete(sys.bucketRemoteTargetRulesMap[bucketName], targetID)
|
||||
if len(sys.bucketRemoteTargetRulesMap[bucketName]) == 0 {
|
||||
delete(sys.bucketRemoteTargetRulesMap, bucketName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sys *NotificationSys) send(bucketName string, eventData event.Event, targetIDs ...event.TargetID) (errs []event.TargetIDErr) {
|
||||
errCh := sys.targetList.Send(eventData, targetIDs...)
|
||||
for terr := range errCh {
|
||||
errs = append(errs, terr)
|
||||
if sys.RemoteTargetExist(bucketName, terr.ID) {
|
||||
sys.RemoveRemoteTarget(bucketName, terr.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Send - sends event data to all matching targets.
|
||||
func (sys *NotificationSys) Send(args eventArgs) []event.TargetIDErr {
|
||||
func (sys *NotificationSys) Send(args eventArgs) {
|
||||
sys.RLock()
|
||||
targetIDSet := sys.bucketRulesMap[args.BucketName].Match(args.EventName, args.Object.Name)
|
||||
sys.RUnlock()
|
||||
|
||||
if len(targetIDSet) == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
targetIDs := targetIDSet.ToSlice()
|
||||
return sys.send(args.BucketName, args.ToEvent(), targetIDs...)
|
||||
sys.targetList.Send(args.ToEvent(true), targetIDSet, sys.targetResCh)
|
||||
}
|
||||
|
||||
// PutBucketObjectLockConfig - put bucket object lock configuration to all peers.
|
||||
func (sys *NotificationSys) PutBucketObjectLockConfig(ctx context.Context, bucketName string, retention objectlock.Retention) {
|
||||
func (sys *NotificationSys) PutBucketObjectLockConfig(ctx context.Context, bucketName string, retention *objectlock.Retention) {
|
||||
g := errgroup.WithNErrs(len(sys.peerClients))
|
||||
for index, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
@@ -835,6 +924,26 @@ func (sys *NotificationSys) PutBucketObjectLockConfig(ctx context.Context, bucke
|
||||
}
|
||||
}
|
||||
|
||||
// PutBucketQuotaConfig - put bucket quota configuration to all peers.
|
||||
func (sys *NotificationSys) PutBucketQuotaConfig(ctx context.Context, bucketName string, q madmin.BucketQuota) {
|
||||
g := errgroup.WithNErrs(len(sys.peerClients))
|
||||
for index, client := range sys.peerClients {
|
||||
if client == nil {
|
||||
continue
|
||||
}
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
return sys.peerClients[index].PutBucketQuotaConfig(bucketName, q)
|
||||
}, index)
|
||||
}
|
||||
for i, err := range g.Wait() {
|
||||
if err != nil {
|
||||
logger.GetReqInfo(ctx).AppendTags("remotePeer", sys.peerClients[i].host.String())
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NetOBDInfo - Net OBD information
|
||||
func (sys *NotificationSys) NetOBDInfo(ctx context.Context) madmin.ServerNetOBDInfo {
|
||||
var sortedGlobalEndpoints []string
|
||||
@@ -1174,6 +1283,7 @@ func NewNotificationSys(endpoints EndpointZones) *NotificationSys {
|
||||
// bucketRulesMap/bucketRemoteTargetRulesMap are initialized by NotificationSys.Init()
|
||||
return &NotificationSys{
|
||||
targetList: event.NewTargetList(),
|
||||
targetResCh: make(chan event.TargetIDResult),
|
||||
bucketRulesMap: make(map[string]event.RulesMap),
|
||||
bucketRemoteTargetRulesMap: make(map[string]map[event.TargetID]event.RulesMap),
|
||||
peerClients: newPeerRestClients(endpoints),
|
||||
@@ -1191,23 +1301,13 @@ type eventArgs struct {
|
||||
}
|
||||
|
||||
// ToEvent - converts to notification event.
|
||||
func (args eventArgs) ToEvent() event.Event {
|
||||
getOriginEndpoint := func() string {
|
||||
host := globalMinioHost
|
||||
if host == "" {
|
||||
// FIXME: Send FQDN or hostname of this machine than sending IP address.
|
||||
host = sortIPs(localIP4.ToSlice())[0]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", getURLScheme(globalIsSSL), net.JoinHostPort(host, globalMinioPort))
|
||||
}
|
||||
|
||||
func (args eventArgs) ToEvent(escape bool) event.Event {
|
||||
eventTime := UTCNow()
|
||||
uniqueID := fmt.Sprintf("%X", eventTime.UnixNano())
|
||||
|
||||
respElements := map[string]string{
|
||||
"x-amz-request-id": args.RespElements["requestId"],
|
||||
"x-minio-origin-endpoint": getOriginEndpoint(), // MinIO specific custom elements.
|
||||
"x-minio-origin-endpoint": globalMinioEndpoint, // MinIO specific custom elements.
|
||||
}
|
||||
// Add deployment as part of
|
||||
if globalDeploymentID != "" {
|
||||
@@ -1216,6 +1316,10 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
if args.RespElements["content-length"] != "" {
|
||||
respElements["content-length"] = args.RespElements["content-length"]
|
||||
}
|
||||
keyName := args.Object.Name
|
||||
if escape {
|
||||
keyName = url.QueryEscape(args.Object.Name)
|
||||
}
|
||||
newEvent := event.Event{
|
||||
EventVersion: "2.0",
|
||||
EventSource: "minio:s3",
|
||||
@@ -1234,7 +1338,7 @@ func (args eventArgs) ToEvent() event.Event {
|
||||
ARN: policy.ResourceARNPrefix + args.BucketName,
|
||||
},
|
||||
Object: event.Object{
|
||||
Key: url.QueryEscape(args.Object.Name),
|
||||
Key: keyName,
|
||||
VersionID: "1",
|
||||
Sequencer: uniqueID,
|
||||
},
|
||||
@@ -1278,19 +1382,10 @@ func sendEvent(args eventArgs) {
|
||||
}
|
||||
|
||||
if globalHTTPListen.HasSubscribers() {
|
||||
globalHTTPListen.Publish(args.ToEvent())
|
||||
globalHTTPListen.Publish(args.ToEvent(false))
|
||||
}
|
||||
|
||||
notifyCh := globalNotificationSys.Send(args)
|
||||
go func() {
|
||||
for _, err := range notifyCh {
|
||||
reqInfo := &logger.ReqInfo{BucketName: args.BucketName, ObjectName: args.Object.Name}
|
||||
reqInfo.AppendTags("EventName", args.EventName.String())
|
||||
reqInfo.AppendTags("targetID", err.ID.Name)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogOnceIf(ctx, err.Err, err.ID)
|
||||
}
|
||||
}()
|
||||
globalNotificationSys.Send(args)
|
||||
}
|
||||
|
||||
func readNotificationConfig(ctx context.Context, objAPI ObjectLayer, bucketName string) (*event.Config, error) {
|
||||
|
||||
@@ -42,7 +42,7 @@ const (
|
||||
)
|
||||
|
||||
// Global object layer mutex, used for safely updating object layer.
|
||||
var globalObjLayerMutex *sync.RWMutex
|
||||
var globalObjLayerMutex sync.RWMutex
|
||||
|
||||
// Global object layer, only accessed by globalObjectAPI.
|
||||
var globalObjectAPI ObjectLayer
|
||||
@@ -50,11 +50,6 @@ var globalObjectAPI ObjectLayer
|
||||
//Global cacheObjects, only accessed by newCacheObjectsFn().
|
||||
var globalCacheObjectAPI CacheObjectLayer
|
||||
|
||||
func init() {
|
||||
// Initialize this once per server initialization.
|
||||
globalObjLayerMutex = &sync.RWMutex{}
|
||||
}
|
||||
|
||||
// Checks if the object is a directory, this logic uses
|
||||
// if size == 0 and object ends with SlashSeparator then
|
||||
// returns true.
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -106,6 +107,8 @@ func toObjectErr(err error, params ...string) error {
|
||||
err = InsufficientWriteQuorum{}
|
||||
case io.ErrUnexpectedEOF, io.ErrShortWrite:
|
||||
err = IncompleteBody{}
|
||||
case context.Canceled, context.DeadlineExceeded:
|
||||
err = IncompleteBody{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -269,6 +272,20 @@ func (e BucketSSEConfigNotFound) Error() string {
|
||||
return "No bucket encryption found for bucket: " + e.Bucket
|
||||
}
|
||||
|
||||
// BucketQuotaConfigNotFound - no bucket quota config found.
|
||||
type BucketQuotaConfigNotFound GenericError
|
||||
|
||||
func (e BucketQuotaConfigNotFound) Error() string {
|
||||
return "No quota config found for bucket : " + e.Bucket
|
||||
}
|
||||
|
||||
// BucketQuotaExceeded - bucket quota exceeded.
|
||||
type BucketQuotaExceeded GenericError
|
||||
|
||||
func (e BucketQuotaExceeded) Error() string {
|
||||
return "Bucket quota exceeded for bucket: " + e.Bucket
|
||||
}
|
||||
|
||||
/// Bucket related errors.
|
||||
|
||||
// BucketNameInvalid - bucketname provided is invalid.
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
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"
|
||||
@@ -60,7 +60,7 @@ type ObjectLayer interface {
|
||||
|
||||
// Storage operations.
|
||||
Shutdown(context.Context) error
|
||||
CrawlAndGetDataUsage(ctx context.Context, updates chan<- DataUsageInfo) error
|
||||
CrawlAndGetDataUsage(ctx context.Context, bf *bloomFilter, updates chan<- DataUsageInfo) error
|
||||
StorageInfo(ctx context.Context, local bool) StorageInfo // local queries only local disks
|
||||
|
||||
// Bucket operations.
|
||||
@@ -138,6 +138,6 @@ type ObjectLayer interface {
|
||||
|
||||
// ObjectTagging operations
|
||||
PutObjectTag(context.Context, string, string, string) error
|
||||
GetObjectTag(context.Context, string, string) (tagging.Tagging, error)
|
||||
GetObjectTag(context.Context, string, string) (*tags.Tags, error)
|
||||
DeleteObjectTag(context.Context, string, string) error
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -842,16 +843,17 @@ func newS2CompressReader(r io.Reader) io.ReadCloser {
|
||||
return pr
|
||||
}
|
||||
|
||||
// Returns error if the cancelCh has been closed (indicating that S3 client has disconnected)
|
||||
type detectDisconnect struct {
|
||||
// Returns error if the context is canceled, indicating
|
||||
// either client has disconnected
|
||||
type contextReader struct {
|
||||
io.ReadCloser
|
||||
cancelCh <-chan struct{}
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (d *detectDisconnect) Read(p []byte) (int, error) {
|
||||
func (d *contextReader) Read(p []byte) (int, error) {
|
||||
select {
|
||||
case <-d.cancelCh:
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
case <-d.ctx.Done():
|
||||
return 0, d.ctx.Err()
|
||||
default:
|
||||
return d.ReadCloser.Read(p)
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func checkPreconditions(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
// Check if the part number is correct.
|
||||
if opts.PartNumber > 0 && opts.PartNumber != len(objInfo.Parts) {
|
||||
if opts.PartNumber > 0 && len(objInfo.Parts) > 0 && opts.PartNumber != len(objInfo.Parts) {
|
||||
writeHeaders()
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrPreconditionFailed), r.URL, guessIsBrowserReq(r))
|
||||
return true
|
||||
|
||||
+57
-110
@@ -34,13 +34,13 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
miniogo "github.com/minio/minio-go/v6"
|
||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||
"github.com/minio/minio-go/v6/pkg/tags"
|
||||
"github.com/minio/minio/cmd/config/etcd/dns"
|
||||
"github.com/minio/minio/cmd/config/storageclass"
|
||||
"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/object/tagging"
|
||||
"github.com/minio/minio/pkg/bucket/policy"
|
||||
"github.com/minio/minio/pkg/event"
|
||||
"github.com/minio/minio/pkg/handlers"
|
||||
@@ -169,25 +169,6 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
s3Select, err := s3select.NewS3Select(r.Body)
|
||||
if err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
encodedErrorResponse := encodeResponse(APIErrorResponse{
|
||||
Code: serr.ErrorCode(),
|
||||
Message: serr.ErrorMessage(),
|
||||
BucketName: bucket,
|
||||
Key: object,
|
||||
Resource: r.URL.Path,
|
||||
RequestID: w.Header().Get(xhttp.AmzRequestID),
|
||||
HostID: globalDeploymentID,
|
||||
})
|
||||
writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
|
||||
} else {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
getObjectNInfo := objectAPI.GetObjectNInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectNInfo = api.CacheAPI().GetObjectNInfo
|
||||
@@ -211,6 +192,26 @@ func (api objectAPIHandlers) SelectObjectContentHandler(w http.ResponseWriter, r
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
s3Select, err := s3select.NewS3Select(r.Body)
|
||||
if err != nil {
|
||||
if serr, ok := err.(s3select.SelectError); ok {
|
||||
encodedErrorResponse := encodeResponse(APIErrorResponse{
|
||||
Code: serr.ErrorCode(),
|
||||
Message: serr.ErrorMessage(),
|
||||
BucketName: bucket,
|
||||
Key: object,
|
||||
Resource: r.URL.Path,
|
||||
RequestID: w.Header().Get(xhttp.AmzRequestID),
|
||||
HostID: globalDeploymentID,
|
||||
})
|
||||
writeResponse(w, serr.HTTPStatusCode(), encodedErrorResponse, mimeXML)
|
||||
} else {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
getRetPerms := checkRequestAuthType(ctx, r, policy.GetObjectRetentionAction, bucket, object)
|
||||
legalHoldPerms := checkRequestAuthType(ctx, r, policy.GetObjectLegalHoldAction, bucket, object)
|
||||
|
||||
@@ -646,24 +647,6 @@ func getCpObjMetadataFromHeader(ctx context.Context, r *http.Request, userMeta m
|
||||
return defaultMeta, nil
|
||||
}
|
||||
|
||||
// Extract tags relevant for an CopyObject operation based on conditional
|
||||
// header values specified in X-Amz-Tagging-Directive.
|
||||
func getCpObjTagsFromHeader(ctx context.Context, r *http.Request, tags string) (string, error) {
|
||||
// if x-amz-tagging-directive says REPLACE then
|
||||
// we extract tags from the input headers.
|
||||
if isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) {
|
||||
if tags := r.Header.Get(xhttp.AmzObjectTagging); tags != "" {
|
||||
return extractTags(ctx, tags)
|
||||
}
|
||||
// Copy is default behavior if x-amz-tagging-directive is set, but x-amz-tagging is
|
||||
// is not set
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Copy is default behavior if x-amz-tagging-directive is not set.
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// getRemoteInstanceTransport contains a singleton roundtripper.
|
||||
var getRemoteInstanceTransport http.RoundTripper
|
||||
var getRemoteInstanceTransportOnce sync.Once
|
||||
@@ -898,6 +881,10 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
length = actualSize
|
||||
}
|
||||
if err := enforceBucketQuota(ctx, dstBucket, actualSize); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
var compressMetadata map[string]string
|
||||
// No need to compress for remote etcd calls
|
||||
@@ -1051,14 +1038,18 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := getCpObjTagsFromHeader(ctx, r, srcInfo.UserTags)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
objTags := srcInfo.UserTags
|
||||
// If x-amz-tagging-directive header is REPLACE, get passed tags.
|
||||
if isDirectiveReplace(r.Header.Get(xhttp.AmzTagDirective)) {
|
||||
objTags = r.Header.Get(xhttp.AmzObjectTagging)
|
||||
if _, err := tags.ParseObjectTags(objTags); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tags != "" {
|
||||
srcInfo.UserDefined[xhttp.AmzObjectTagging] = tags
|
||||
if objTags != "" {
|
||||
srcInfo.UserDefined[xhttp.AmzObjectTagging] = objTags
|
||||
}
|
||||
|
||||
srcInfo.UserDefined = objectlock.FilterObjectLockMetadata(srcInfo.UserDefined, true, true)
|
||||
@@ -1204,7 +1195,7 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
// To detect if the client has disconnected.
|
||||
r.Body = &detectDisconnect{r.Body, r.Context().Done()}
|
||||
r.Body = &contextReader{r.Body, r.Context()}
|
||||
|
||||
// X-Amz-Copy-Source shouldn't be set for this call.
|
||||
if _, ok := r.Header[xhttp.AmzCopySource]; ok {
|
||||
@@ -1260,12 +1251,13 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if tags := r.Header.Get(xhttp.AmzObjectTagging); tags != "" {
|
||||
metadata[xhttp.AmzObjectTagging], err = extractTags(ctx, tags)
|
||||
if err != nil {
|
||||
if objTags := r.Header.Get(xhttp.AmzObjectTagging); objTags != "" {
|
||||
if _, err := tags.ParseObjectTags(objTags); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
metadata[xhttp.AmzObjectTagging] = objTags
|
||||
}
|
||||
|
||||
if rAuthType == authTypeStreamingSigned {
|
||||
@@ -1327,7 +1319,10 @@ func (api objectAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Req
|
||||
sha256hex = getContentSha256Cksum(r, serviceS3)
|
||||
}
|
||||
}
|
||||
|
||||
if err := enforceBucketQuota(ctx, bucket, size); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Check if bucket encryption is enabled
|
||||
_, encEnabled := globalBucketSSEConfigSys.Get(bucket)
|
||||
// This request header needs to be set prior to setting ObjectOptions
|
||||
@@ -1526,14 +1521,6 @@ func (api objectAPIHandlers) NewMultipartUploadHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate storage class metadata if present
|
||||
if sc := r.Header.Get(xhttp.AmzStorageClass); sc != "" {
|
||||
if !storageclass.IsValid(sc) {
|
||||
@@ -1724,14 +1711,6 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if global WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err := objectAPI.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
getObjectNInfo := objectAPI.GetObjectNInfo
|
||||
if api.CacheAPI() != nil {
|
||||
getObjectNInfo = api.CacheAPI().GetObjectNInfo
|
||||
@@ -1777,7 +1756,10 @@ func (api objectAPIHandlers) CopyObjectPartHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := enforceBucketQuota(ctx, dstBucket, actualPartSize); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
// Special care for CopyObjectPart
|
||||
if partRangeErr := checkCopyPartRangeWithSize(rs, actualPartSize); partRangeErr != nil {
|
||||
writeCopyPartErr(ctx, w, partRangeErr, r.URL, guessIsBrowserReq(r))
|
||||
@@ -1963,7 +1945,7 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
|
||||
// To detect if the client has disconnected.
|
||||
r.Body = &detectDisconnect{r.Body, r.Context().Done()}
|
||||
r.Body = &contextReader{r.Body, r.Context()}
|
||||
|
||||
// X-Amz-Copy-Source shouldn't be set for this call.
|
||||
if _, ok := r.Header[xhttp.AmzCopySource]; ok {
|
||||
@@ -2058,6 +2040,11 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
}
|
||||
}
|
||||
|
||||
if err := enforceBucketQuota(ctx, bucket, size); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
actualSize := size
|
||||
|
||||
// get encryption options
|
||||
@@ -2104,14 +2091,6 @@ func (api objectAPIHandlers) PutObjectPartHandler(w http.ResponseWriter, r *http
|
||||
rawReader := hashReader
|
||||
pReader := NewPutObjReader(rawReader, nil, nil)
|
||||
|
||||
// Deny if WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isEncrypted := false
|
||||
var objectEncryptionKey crypto.ObjectKey
|
||||
if objectAPI.IsEncryptionSupported() && !isCompressed {
|
||||
@@ -2429,19 +2408,6 @@ func (api objectAPIHandlers) CompleteMultipartUploadHandler(w http.ResponseWrite
|
||||
return
|
||||
}
|
||||
|
||||
// Deny if global WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, _, _, s3Err := checkPutObjectLockAllowed(ctx, r, bucket, object, objectAPI.GetObjectInfo, ErrNone, ErrNone); s3Err != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -2626,19 +2592,6 @@ func (api objectAPIHandlers) DeleteObjectHandler(w http.ResponseWriter, r *http.
|
||||
}
|
||||
}
|
||||
|
||||
// Deny if global WORM is enabled
|
||||
if globalWORMEnabled {
|
||||
opts, err := getOpts(ctx, r, bucket, object)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
if _, err := objectAPI.GetObjectInfo(ctx, bucket, object, opts); err == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
apiErr := ErrNone
|
||||
if _, ok := globalBucketObjectLockConfig.Get(bucket); ok {
|
||||
apiErr = enforceRetentionBypassForDelete(ctx, r, bucket, object, getObjectInfo)
|
||||
@@ -2863,11 +2816,6 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if globalWORMEnabled {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrObjectLocked), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := globalBucketObjectLockConfig.Get(bucket); !ok {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidBucketObjectLockConfiguration), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -3033,14 +2981,14 @@ func (api objectAPIHandlers) PutObjectTaggingHandler(w http.ResponseWriter, r *h
|
||||
return
|
||||
}
|
||||
|
||||
tagging, err := tagging.ParseTagging(io.LimitReader(r.Body, r.ContentLength))
|
||||
tags, err := tags.ParseObjectXML(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
// Put object tags
|
||||
err = objAPI.PutObjectTag(ctx, bucket, object, tagging.String())
|
||||
err = objAPI.PutObjectTag(ctx, bucket, object, tags.String())
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
@@ -3075,8 +3023,7 @@ func (api objectAPIHandlers) DeleteObjectTaggingHandler(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
// Delete object tags
|
||||
err = objAPI.DeleteObjectTag(ctx, bucket, object)
|
||||
if err != nil {
|
||||
if err = objAPI.DeleteObjectTag(ctx, bucket, object); err != nil && err != errConfigNotFound {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL, guessIsBrowserReq(r))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* MinIO Cloud Storage, (C) 2018, 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 cmd
|
||||
|
||||
import "github.com/minio/minio/pkg/event"
|
||||
|
||||
// PeerRESTClientTarget - RPCClient is an event.Target which sends event to target of remote peer.
|
||||
type PeerRESTClientTarget struct {
|
||||
id event.TargetID
|
||||
remoteTargetID event.TargetID
|
||||
restClient *peerRESTClient
|
||||
bucketName string
|
||||
}
|
||||
|
||||
// ID - returns target ID.
|
||||
func (target *PeerRESTClientTarget) ID() event.TargetID {
|
||||
return target.id
|
||||
}
|
||||
|
||||
// IsActive - does nothing and available for interface compatibility.
|
||||
func (target *PeerRESTClientTarget) IsActive() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// HasQueueStore - No-Op. Added for interface compatibility
|
||||
func (target PeerRESTClientTarget) HasQueueStore() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Save - Sends event directly without persisting.
|
||||
func (target *PeerRESTClientTarget) Save(eventData event.Event) error {
|
||||
return target.send(eventData)
|
||||
}
|
||||
|
||||
// Send - interface compatible method does no-op.
|
||||
func (target *PeerRESTClientTarget) Send(eventKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sends event to remote peer by making RPC call.
|
||||
func (target *PeerRESTClientTarget) send(eventData event.Event) error {
|
||||
return target.restClient.SendEvent(target.bucketName, target.id, target.remoteTargetID, eventData)
|
||||
}
|
||||
|
||||
// Close - does nothing and available for interface compatibility.
|
||||
func (target *PeerRESTClientTarget) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPeerRESTClientTarget - creates RPCClient target with given target ID available in remote peer.
|
||||
func NewPeerRESTClientTarget(bucketName string, targetID event.TargetID, restClient *peerRESTClient) *PeerRESTClientTarget {
|
||||
return &PeerRESTClientTarget{
|
||||
id: event.TargetID{ID: targetID.ID, Name: targetID.Name + "+" + mustGetUUID()},
|
||||
remoteTargetID: targetID,
|
||||
bucketName: bucketName,
|
||||
restClient: restClient,
|
||||
}
|
||||
}
|
||||
+78
-74
@@ -500,78 +500,6 @@ func (client *peerRESTClient) ReloadFormat(dryRun bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEvent - calls send event RPC.
|
||||
func (client *peerRESTClient) SendEvent(bucket string, targetID, remoteTargetID event.TargetID, eventData event.Event) error {
|
||||
numTries := 10
|
||||
for {
|
||||
err := client.sendEvent(bucket, targetID, remoteTargetID, eventData)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if numTries == 0 {
|
||||
return err
|
||||
}
|
||||
numTries--
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func (client *peerRESTClient) sendEvent(bucket string, targetID, remoteTargetID event.TargetID, eventData event.Event) error {
|
||||
args := sendEventRequest{
|
||||
TargetID: remoteTargetID,
|
||||
Event: eventData,
|
||||
}
|
||||
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
|
||||
var reader bytes.Buffer
|
||||
err := gob.NewEncoder(&reader).Encode(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
respBody, err := client.call(peerRESTMethodSendEvent, values, &reader, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var eventResp sendEventResp
|
||||
defer http.DrainBody(respBody)
|
||||
err = gob.NewDecoder(respBody).Decode(&eventResp)
|
||||
|
||||
if err != nil || !eventResp.Success {
|
||||
reqInfo := &logger.ReqInfo{BucketName: bucket}
|
||||
reqInfo.AppendTags("targetID", targetID.Name)
|
||||
reqInfo.AppendTags("event", eventData.EventName.String())
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
globalNotificationSys.RemoveRemoteTarget(bucket, targetID)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoteTargetExist - calls remote target ID exist REST API.
|
||||
func (client *peerRESTClient) RemoteTargetExist(bucket string, targetID event.TargetID) (bool, error) {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
|
||||
var reader bytes.Buffer
|
||||
err := gob.NewEncoder(&reader).Encode(targetID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
respBody, err := client.call(peerRESTMethodTargetExists, values, &reader, -1)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
var targetExists remoteTargetExistsResp
|
||||
err = gob.NewDecoder(respBody).Decode(&targetExists)
|
||||
return targetExists.Exists, err
|
||||
}
|
||||
|
||||
// RemoveBucketPolicy - Remove bucket policy on the peer node.
|
||||
func (client *peerRESTClient) RemoveBucketPolicy(bucket string) error {
|
||||
values := make(url.Values)
|
||||
@@ -596,6 +524,37 @@ func (client *peerRESTClient) RemoveBucketObjectLockConfig(bucket string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// cycleServerBloomFilter will cycle the bloom filter to start recording to index y if not already.
|
||||
// The response will contain a bloom filter starting at index x up to, but not including index y.
|
||||
// If y is 0, the response will not update y, but return the currently recorded information
|
||||
// from the current x to y-1.
|
||||
func (client *peerRESTClient) cycleServerBloomFilter(ctx context.Context, req bloomFilterRequest) (*bloomFilterResponse, error) {
|
||||
var reader bytes.Buffer
|
||||
err := gob.NewEncoder(&reader).Encode(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := client.call(peerRESTMethodCycleBloom, nil, &reader, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp bloomFilterResponse
|
||||
defer http.DrainBody(respBody)
|
||||
return &resp, gob.NewDecoder(respBody).Decode(&resp)
|
||||
}
|
||||
|
||||
// RemoveBucketQuotaConfig - Remove bucket quota config on the peer node.
|
||||
func (client *peerRESTClient) RemoveBucketQuotaConfig(bucket string) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
respBody, err := client.call(peerRESTMethodBucketQuotaConfigRemove, values, nil, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetBucketPolicy - Set bucket policy on the peer node.
|
||||
func (client *peerRESTClient) SetBucketPolicy(bucket string, bucketPolicy *policy.Policy) error {
|
||||
values := make(url.Values)
|
||||
@@ -697,12 +656,12 @@ func (client *peerRESTClient) PutBucketNotification(bucket string, rulesMap even
|
||||
}
|
||||
|
||||
// PutBucketObjectLockConfig - PUT bucket object lock configuration.
|
||||
func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention objectlock.Retention) error {
|
||||
func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention *objectlock.Retention) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
|
||||
var reader bytes.Buffer
|
||||
err := gob.NewEncoder(&reader).Encode(&retention)
|
||||
err := gob.NewEncoder(&reader).Encode(retention)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -715,6 +674,25 @@ func (client *peerRESTClient) PutBucketObjectLockConfig(bucket string, retention
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutBucketQuotaConfig - PUT bucket quota configuration.
|
||||
func (client *peerRESTClient) PutBucketQuotaConfig(bucket string, q madmin.BucketQuota) error {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTBucket, bucket)
|
||||
|
||||
var reader bytes.Buffer
|
||||
err := gob.NewEncoder(&reader).Encode(&q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
respBody, err := client.call(peerRESTMethodPutBucketQuotaConfig, values, &reader, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeletePolicy - delete a specific canned policy.
|
||||
func (client *peerRESTClient) DeletePolicy(policyName string) (err error) {
|
||||
values := make(url.Values)
|
||||
@@ -770,6 +748,19 @@ func (client *peerRESTClient) DeleteUser(accessKey string) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteServiceAccount - delete a specific service account.
|
||||
func (client *peerRESTClient) DeleteServiceAccount(accessKey string) (err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTUser, accessKey)
|
||||
|
||||
respBody, err := client.call(peerRESTMethodDeleteServiceAccount, values, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadUser - reload a specific user.
|
||||
func (client *peerRESTClient) LoadUser(accessKey string, temp bool) (err error) {
|
||||
values := make(url.Values)
|
||||
@@ -784,6 +775,19 @@ func (client *peerRESTClient) LoadUser(accessKey string, temp bool) (err error)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadServiceAccount - reload a specific service account.
|
||||
func (client *peerRESTClient) LoadServiceAccount(accessKey string) (err error) {
|
||||
values := make(url.Values)
|
||||
values.Set(peerRESTUser, accessKey)
|
||||
|
||||
respBody, err := client.call(peerRESTMethodLoadServiceAccount, values, nil, -1)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer http.DrainBody(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadGroup - send load group command to peers.
|
||||
func (client *peerRESTClient) LoadGroup(group string) error {
|
||||
values := make(url.Values)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
package cmd
|
||||
|
||||
const (
|
||||
peerRESTVersion = "v7"
|
||||
peerRESTVersion = "v8"
|
||||
peerRESTVersionPrefix = SlashSeparator + peerRESTVersion
|
||||
peerRESTPrefix = minioReservedBucketPath + "/peer"
|
||||
peerRESTPath = peerRESTPrefix + peerRESTVersionPrefix
|
||||
@@ -40,7 +40,9 @@ const (
|
||||
peerRESTMethodGetLocks = "/getlocks"
|
||||
peerRESTMethodBucketPolicyRemove = "/removebucketpolicy"
|
||||
peerRESTMethodLoadUser = "/loaduser"
|
||||
peerRESTMethodLoadServiceAccount = "/loadserviceaccount"
|
||||
peerRESTMethodDeleteUser = "/deleteuser"
|
||||
peerRESTMethodDeleteServiceAccount = "/deleteserviceaccount"
|
||||
peerRESTMethodLoadPolicy = "/loadpolicy"
|
||||
peerRESTMethodLoadPolicyMapping = "/loadpolicymapping"
|
||||
peerRESTMethodDeletePolicy = "/deletepolicy"
|
||||
@@ -50,19 +52,19 @@ const (
|
||||
peerRESTMethodDownloadProfilingData = "/downloadprofilingdata"
|
||||
peerRESTMethodBucketPolicySet = "/setbucketpolicy"
|
||||
peerRESTMethodBucketNotificationPut = "/putbucketnotification"
|
||||
peerRESTMethodBucketNotificationListen = "/listenbucketnotification"
|
||||
peerRESTMethodReloadFormat = "/reloadformat"
|
||||
peerRESTMethodTargetExists = "/targetexists"
|
||||
peerRESTMethodSendEvent = "/sendevent"
|
||||
peerRESTMethodCycleBloom = "/cyclebloom"
|
||||
peerRESTMethodTrace = "/trace"
|
||||
peerRESTMethodListen = "/listen"
|
||||
peerRESTMethodLog = "/log"
|
||||
peerRESTMethodBucketLifecycleSet = "/setbucketlifecycle"
|
||||
peerRESTMethodBucketLifecycleRemove = "/removebucketlifecycle"
|
||||
peerRESTMethodBucketEncryptionSet = "/setbucketencryption"
|
||||
peerRESTMethodBucketEncryptionRemove = "/removebucketencryption"
|
||||
peerRESTMethodLog = "/log"
|
||||
peerRESTMethodPutBucketObjectLockConfig = "/putbucketobjectlockconfig"
|
||||
peerRESTMethodBucketObjectLockConfigRemove = "/removebucketobjectlockconfig"
|
||||
peerRESTMethodPutBucketQuotaConfig = "/putbucketquotaconfig"
|
||||
peerRESTMethodBucketQuotaConfigRemove = "/removebucketquotaconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
+137
-90
@@ -24,7 +24,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -184,6 +183,72 @@ func (s *peerRESTServer) LoadPolicyMappingHandler(w http.ResponseWriter, r *http
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DeleteServiceAccountHandler - deletes a service account on the server.
|
||||
func (s *peerRESTServer) DeleteServiceAccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
s.writeErrorResponse(w, errServerNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
if globalIAMSys == nil {
|
||||
s.writeErrorResponse(w, errServerNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars[peerRESTUser]
|
||||
if accessKey == "" {
|
||||
s.writeErrorResponse(w, errors.New("service account name is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalIAMSys.DeleteServiceAccount(context.Background(), accessKey); err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// LoadServiceAccountHandler - reloads a service account on the server.
|
||||
func (s *peerRESTServer) LoadServiceAccountHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := newObjectLayerWithoutSafeModeFn()
|
||||
if objAPI == nil {
|
||||
s.writeErrorResponse(w, errServerNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
if globalIAMSys == nil {
|
||||
s.writeErrorResponse(w, errServerNotInitialized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
accessKey := vars[peerRESTUser]
|
||||
if accessKey == "" {
|
||||
s.writeErrorResponse(w, errors.New("service account parameter is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalIAMSys.LoadServiceAccount(accessKey); err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// DeleteUserHandler - deletes a user on the server.
|
||||
func (s *peerRESTServer) DeleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -534,6 +599,7 @@ func (s *peerRESTServer) DeleteBucketHandler(w http.ResponseWriter, r *http.Requ
|
||||
globalNotificationSys.RemoveNotification(bucketName)
|
||||
globalPolicySys.Remove(bucketName)
|
||||
globalBucketObjectLockConfig.Remove(bucketName)
|
||||
globalBucketQuotaSys.Remove(bucketName)
|
||||
globalLifecycleSys.Remove(bucketName)
|
||||
|
||||
w.(http.Flusher).Flush()
|
||||
@@ -609,13 +675,14 @@ func (s *peerRESTServer) SetBucketPolicyHandler(w http.ResponseWriter, r *http.R
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
var policyData policy.Policy
|
||||
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&policyData)
|
||||
var policyData = &policy.Policy{}
|
||||
err := gob.NewDecoder(r.Body).Decode(policyData)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -650,13 +717,13 @@ func (s *peerRESTServer) SetBucketLifecycleHandler(w http.ResponseWriter, r *htt
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
var lifecycleData lifecycle.Lifecycle
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&lifecycleData)
|
||||
var lifecycleData = &lifecycle.Lifecycle{}
|
||||
err := gob.NewDecoder(r.Body).Decode(lifecycleData)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -692,13 +759,13 @@ func (s *peerRESTServer) SetBucketSSEConfigHandler(w http.ResponseWriter, r *htt
|
||||
return
|
||||
}
|
||||
|
||||
var encConfig bucketsse.BucketSSEConfig
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&encConfig)
|
||||
var encConfig = &bucketsse.BucketSSEConfig{}
|
||||
err := gob.NewDecoder(r.Body).Decode(encConfig)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -708,94 +775,27 @@ func (s *peerRESTServer) SetBucketSSEConfigHandler(w http.ResponseWriter, r *htt
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
type remoteTargetExistsResp struct {
|
||||
Exists bool
|
||||
}
|
||||
|
||||
// TargetExistsHandler - Check if Target exists.
|
||||
func (s *peerRESTServer) TargetExistsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "TargetExists")
|
||||
// CycleServerBloomFilterHandler cycles bllom filter on server.
|
||||
func (s *peerRESTServer) CycleServerBloomFilterHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars[peerRESTBucket]
|
||||
if bucketName == "" {
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
var targetID event.TargetID
|
||||
if r.ContentLength <= 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
ctx := newContext(r, w, "CycleServerBloomFilter")
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&targetID)
|
||||
var req bloomFilterRequest
|
||||
err := gob.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var targetExists remoteTargetExistsResp
|
||||
targetExists.Exists = globalNotificationSys.RemoteTargetExist(bucketName, targetID)
|
||||
|
||||
defer w.(http.Flusher).Flush()
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(&targetExists))
|
||||
}
|
||||
|
||||
type sendEventRequest struct {
|
||||
Event event.Event
|
||||
TargetID event.TargetID
|
||||
}
|
||||
|
||||
type sendEventResp struct {
|
||||
Success bool
|
||||
}
|
||||
|
||||
// SendEventHandler - Send Event.
|
||||
func (s *peerRESTServer) SendEventHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := newContext(r, w, "SendEvent")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars[peerRESTBucket]
|
||||
if bucketName == "" {
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
var eventReq sendEventRequest
|
||||
if r.ContentLength <= 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&eventReq)
|
||||
bf, err := intDataUpdateTracker.cycleFilter(ctx, req.Oldest, req.Current)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var eventResp sendEventResp
|
||||
eventResp.Success = true
|
||||
errs := globalNotificationSys.send(bucketName, eventReq.Event, eventReq.TargetID)
|
||||
|
||||
for i := range errs {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("Event", eventReq.Event.EventName.String())
|
||||
reqInfo.AppendTags("targetName", eventReq.TargetID.Name)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogIf(ctx, errs[i].Err)
|
||||
|
||||
eventResp.Success = false
|
||||
s.writeErrorResponse(w, errs[i].Err)
|
||||
return
|
||||
}
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(&eventResp))
|
||||
logger.LogIf(ctx, gob.NewEncoder(w).Encode(bf))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
@@ -861,13 +861,13 @@ func (s *peerRESTServer) PutBucketObjectLockConfigHandler(w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
var retention objectlock.Retention
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode(&retention)
|
||||
var retention = &objectlock.Retention{}
|
||||
err := gob.NewDecoder(r.Body).Decode(retention)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
@@ -877,6 +877,54 @@ func (s *peerRESTServer) PutBucketObjectLockConfigHandler(w http.ResponseWriter,
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// PutBucketQuotaConfigHandler - handles PUT bucket quota configuration.
|
||||
func (s *peerRESTServer) PutBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars[peerRESTBucket]
|
||||
if bucketName == "" {
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
var quota madmin.BucketQuota
|
||||
if r.ContentLength < 0 {
|
||||
s.writeErrorResponse(w, errInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
err := gob.NewDecoder(r.Body).Decode("a)
|
||||
if err != nil {
|
||||
s.writeErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
globalBucketQuotaSys.Set(bucketName, quota)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// RemoveBucketQuotaConfigHandler - handles DELETE bucket quota configuration.
|
||||
func (s *peerRESTServer) RemoveBucketQuotaConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
s.writeErrorResponse(w, errors.New("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucketName := vars[peerRESTBucket]
|
||||
if bucketName == "" {
|
||||
s.writeErrorResponse(w, errors.New("Bucket name is missing"))
|
||||
return
|
||||
}
|
||||
|
||||
globalBucketQuotaSys.Remove(bucketName)
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
// ServerUpdateHandler - updates the current server.
|
||||
func (s *peerRESTServer) ServerUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.IsValid(w, r) {
|
||||
@@ -1011,11 +1059,7 @@ func (s *peerRESTServer) ListenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if ev.S3.Bucket.Name != values.Get(peerRESTListenBucket) {
|
||||
return false
|
||||
}
|
||||
objectName, uerr := url.QueryUnescape(ev.S3.Object.Key)
|
||||
if uerr != nil {
|
||||
objectName = ev.S3.Object.Key
|
||||
}
|
||||
return len(rulesMap.Match(ev.EventName, objectName).ToSlice()) != 0
|
||||
return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
|
||||
})
|
||||
|
||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||
@@ -1154,6 +1198,7 @@ func registerPeerRESTHandlers(router *mux.Router) {
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDriveOBDInfo).HandlerFunc(httpTraceHdrs(server.DriveOBDInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodNetOBDInfo).HandlerFunc(httpTraceHdrs(server.NetOBDInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDispatchNetOBDInfo).HandlerFunc(httpTraceHdrs(server.DispatchNetOBDInfoHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodCycleBloom).HandlerFunc(httpTraceHdrs(server.CycleServerBloomFilterHandler))
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDeleteBucket).HandlerFunc(httpTraceHdrs(server.DeleteBucketHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSignalService).HandlerFunc(httpTraceHdrs(server.SignalServiceHandler)).Queries(restQueries(peerRESTSignal)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodServerUpdate).HandlerFunc(httpTraceHdrs(server.ServerUpdateHandler)).Queries(restQueries(peerRESTUpdateURL, peerRESTSha256Hex, peerRESTLatestRelease)...)
|
||||
@@ -1165,14 +1210,14 @@ func registerPeerRESTHandlers(router *mux.Router) {
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadPolicy).HandlerFunc(httpTraceAll(server.LoadPolicyHandler)).Queries(restQueries(peerRESTPolicy)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadPolicyMapping).HandlerFunc(httpTraceAll(server.LoadPolicyMappingHandler)).Queries(restQueries(peerRESTUserOrGroup)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDeleteUser).HandlerFunc(httpTraceAll(server.DeleteUserHandler)).Queries(restQueries(peerRESTUser)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDeleteServiceAccount).HandlerFunc(httpTraceAll(server.DeleteServiceAccountHandler)).Queries(restQueries(peerRESTUser)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadUser).HandlerFunc(httpTraceAll(server.LoadUserHandler)).Queries(restQueries(peerRESTUser, peerRESTUserTemp)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadServiceAccount).HandlerFunc(httpTraceAll(server.LoadServiceAccountHandler)).Queries(restQueries(peerRESTUser)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLoadGroup).HandlerFunc(httpTraceAll(server.LoadGroupHandler)).Queries(restQueries(peerRESTGroup)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodStartProfiling).HandlerFunc(httpTraceAll(server.StartProfilingHandler)).Queries(restQueries(peerRESTProfiler)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodDownloadProfilingData).HandlerFunc(httpTraceHdrs(server.DownloadProfilingDataHandler))
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodTargetExists).HandlerFunc(httpTraceHdrs(server.TargetExistsHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodSendEvent).HandlerFunc(httpTraceHdrs(server.SendEventHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketNotificationPut).HandlerFunc(httpTraceHdrs(server.PutBucketNotificationHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodReloadFormat).HandlerFunc(httpTraceHdrs(server.ReloadFormatHandler)).Queries(restQueries(peerRESTDryRun)...)
|
||||
@@ -1186,4 +1231,6 @@ func registerPeerRESTHandlers(router *mux.Router) {
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodLog).HandlerFunc(server.ConsoleLogHandler)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodPutBucketObjectLockConfig).HandlerFunc(httpTraceHdrs(server.PutBucketObjectLockConfigHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketObjectLockConfigRemove).HandlerFunc(httpTraceHdrs(server.RemoveBucketObjectLockConfigHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodPutBucketQuotaConfig).HandlerFunc(httpTraceHdrs(server.PutBucketQuotaConfigHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
subrouter.Methods(http.MethodPost).Path(peerRESTVersionPrefix + peerRESTMethodBucketQuotaConfigRemove).HandlerFunc(httpTraceHdrs(server.RemoveBucketQuotaConfigHandler)).Queries(restQueries(peerRESTBucket)...)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user