mirror of
https://github.com/pgsty/minio.git
synced 2026-11-07 00:02:11 +03:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e2d4d097e7 | |||
| ac8cb6ba0d | |||
| 4ce81fd07f | |||
| df9eeb7f8f | |||
| 31c4fdbf79 | |||
| 48e367ff7d | |||
| fd02492cb7 | |||
| addfa35d93 | |||
| 5afdc56796 | |||
| fb1c333a83 | |||
| c3e1da8e04 | |||
| 20a753e2e5 | |||
| 3a398775fb | |||
| 61a7434379 | |||
| 09f5e29327 | |||
| 29edb4ccfe | |||
| 197d6fb644 | |||
| d4e565e595 | |||
| 1fce2b180f | |||
| be6ccd129d | |||
| f7cecf0945 | |||
| 7b2198f7e5 | |||
| 52221db7ef | |||
| befbf48563 | |||
| 56a61bab56 | |||
| d480022711 | |||
| f1abb92f0c | |||
| 5792be71fa | |||
| c2630bb3a3 | |||
| 5e3010d455 | |||
| ccbf65c8e8 | |||
| 9d07cde385 | |||
| 464b9d7c80 | |||
| 5c81d0d89a | |||
| 62cd643868 | |||
| c0bf02b8b2 | |||
| 1b7dd70f72 | |||
| 372a08be49 | |||
| fd46a1c3b3 | |||
| 5aae7178ad | |||
| dea8220eee | |||
| a4be0b88f6 | |||
| d8101573be | |||
| 41cdb357bb | |||
| 38caddffe7 | |||
| 80fe166902 | |||
| 0e26f983d6 | |||
| fc08fcab52 | |||
| 77dc99e71d | |||
| 5041bfcb5c | |||
| 5be76856bd | |||
| 2a3f5e1ad1 | |||
| f8650a3493 | |||
| 90a52a29c5 | |||
| 8859c92f80 | |||
| 01e5632949 | |||
| 18a4276e25 | |||
| c06032f35f | |||
| 95a6b2c991 | |||
| ee28f6caaa | |||
| 30c9e50701 | |||
| 9aadd725d2 | |||
| 6cfb1cb6fd | |||
| 2dc8ac1e62 | |||
| e952e2a691 | |||
| 040ac5cad8 | |||
| 05685863e3 | |||
| d324c0a1c3 | |||
| 03f8b25b50 | |||
| b0e2c2da78 | |||
| f28a8eca91 | |||
| ca69e54cb6 | |||
| 4629abd5a2 | |||
| dc99f4a7a3 | |||
| 389ec21d0c | |||
| 9341201132 | |||
| 88dd83a365 | |||
| 74285d50c4 | |||
| 60d0611ac2 | |||
| f939222942 | |||
| c293c2e9a3 | |||
| e34ca9acd1 | |||
| 83071a3459 | |||
| edf364bf21 | |||
| 1e037883b0 | |||
| d909f167ff | |||
| 4592aaa3e2 | |||
| 95d1a12422 | |||
| 62aa42cccf | |||
| 5cffd3780a | |||
| def75ffcfe | |||
| ad8e611098 | |||
| 3ec1844e4a | |||
| 523670ba0d | |||
| 35dea24ffd | |||
| e55104a155 | |||
| 111745c564 | |||
| c7df1ffc6f | |||
| 2b7e75e079 | |||
| bcdaa09c75 | |||
| 2fc65dcb99 | |||
| 44a3b58e52 | |||
| 0a256053ee | |||
| 46de9ac03e | |||
| a53dc1d9c8 | |||
| f0462322fd | |||
| c59d2a6288 | |||
| 3e3ff2a70b | |||
| 16bc11e72e | |||
| 2719f1efaa | |||
| cff1be0ae8 | |||
| 39ac62a1a1 | |||
| f427dbbd60 | |||
| c3f689a7d9 | |||
| 85f3a9f3b0 | |||
| 13ba4b433d | |||
| 96f27a4965 |
@@ -0,0 +1,51 @@
|
||||
name: FIPS Build Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||
# updated.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Go BoringCrypto ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17.11b7, 1.18.3b7]
|
||||
os: [ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Setup dockerfile for build test
|
||||
run: |
|
||||
echo "FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:${{ matrix.go-version }}" > Dockerfile.fips.test
|
||||
echo "COPY . /minio" >> Dockerfile.fips.test
|
||||
echo "WORKDIR /minio" >> Dockerfile.fips.test
|
||||
echo "RUN make" >> Dockerfile.fips.test
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.fips.test
|
||||
push: false
|
||||
load: true
|
||||
tags: minio/fips-test:latest
|
||||
|
||||
# This should fail if grep returns non-zero exit
|
||||
- name: Test binary
|
||||
run: |
|
||||
docker run --rm minio/fips-test:latest ./minio --version
|
||||
docker run --rm -i minio/fips-test:latest /bin/bash -c 'go tool nm ./minio' | grep -q FIPS
|
||||
@@ -108,6 +108,17 @@ jobs:
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
make test-iam
|
||||
- name: Test with Access Management Plugin enabled
|
||||
env:
|
||||
LDAP_TEST_SERVER: ${{ matrix.ldap }}
|
||||
ETCD_SERVER: ${{ matrix.etcd }}
|
||||
OPENID_TEST_SERVER: ${{ matrix.openid }}
|
||||
POLICY_PLUGIN_ENDPOINT: "http://127.0.0.1:8080"
|
||||
run: |
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
go run docs/iam/access-manager-plugin.go &
|
||||
make test-iam
|
||||
- name: Test LDAP for automatic site replication
|
||||
if: matrix.ldap == 'localhost:389'
|
||||
run: |
|
||||
|
||||
@@ -2124,57 +2124,6 @@ SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/PuerkitoBio/purell
|
||||
https://github.com/PuerkitoBio/purell
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012, Martin Angers
|
||||
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 author 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 HOLDER 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/PuerkitoBio/urlesc
|
||||
https://github.com/PuerkitoBio/urlesc
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/Shopify/sarama
|
||||
https://github.com/Shopify/sarama
|
||||
----------------------------------------------------------------
|
||||
@@ -2792,6 +2741,186 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/briandowns/spinner
|
||||
https://github.com/briandowns/spinner
|
||||
----------------------------------------------------------------
|
||||
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.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/buger/jsonparser
|
||||
https://github.com/buger/jsonparser
|
||||
----------------------------------------------------------------
|
||||
@@ -2945,34 +3074,6 @@ Redistribution and use in source and binary forms, with or without modification,
|
||||
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 HOLDER 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/colinmarc/hdfs/v2
|
||||
https://github.com/colinmarc/hdfs/v2
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2014 Colin Marc (colinmarc@gmail.com)
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/containerd/console
|
||||
https://github.com/containerd/console
|
||||
----------------------------------------------------------------
|
||||
@@ -5140,6 +5241,214 @@ SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/fraugster/parquet-go
|
||||
https://github.com/fraugster/parquet-go
|
||||
----------------------------------------------------------------
|
||||
|
||||
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/gdamore/encoding
|
||||
https://github.com/gdamore/encoding
|
||||
----------------------------------------------------------------
|
||||
@@ -8191,21 +8500,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/golang-jwt/jwt
|
||||
https://github.com/golang-jwt/jwt
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012 Dave Grijalva
|
||||
Copyright (c) 2021 golang-jwt maintainers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/golang-jwt/jwt/v4
|
||||
@@ -9634,72 +9928,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/gorilla/securecookie
|
||||
https://github.com/gorilla/securecookie
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012 Rodrigo Moraes. 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/gorilla/sessions
|
||||
https://github.com/gorilla/sessions
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/gorilla/websocket
|
||||
https://github.com/gorilla/websocket
|
||||
----------------------------------------------------------------
|
||||
@@ -17719,214 +17947,6 @@ https://github.com/minio/minio-go/v7
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/parquet-go
|
||||
https://github.com/minio/parquet-go
|
||||
----------------------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/minio/pkg
|
||||
https://github.com/minio/pkg
|
||||
----------------------------------------------------------------
|
||||
@@ -23311,6 +23331,39 @@ SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/rogpeppe/go-internal
|
||||
https://github.com/rogpeppe/go-internal
|
||||
----------------------------------------------------------------
|
||||
Copyright (c) 2018 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/rs/cors
|
||||
https://github.com/rs/cors
|
||||
----------------------------------------------------------------
|
||||
@@ -23670,33 +23723,6 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/tidwall/sjson
|
||||
https://github.com/tidwall/sjson
|
||||
----------------------------------------------------------------
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Josh Baker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
================================================================
|
||||
|
||||
github.com/tinylib/msgp
|
||||
|
||||
@@ -53,11 +53,12 @@ test-iam: build ## verify IAM (external IDP, etcd backends)
|
||||
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||
@CGO_ENABLED=0 go test -tags kqueue -v -run TestIAM* ./cmd
|
||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||
@CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go test -race -tags kqueue -v -run TestIAM* ./cmd
|
||||
|
||||
test-replication: install ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||
|
||||
test-site-replication-ldap: install ## verify automatic site replication
|
||||
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
||||
@@ -73,18 +74,18 @@ test-site-replication-minio: install ## verify automatic site replication
|
||||
|
||||
verify: ## verify minio various setups
|
||||
@echo "Verifying build with race"
|
||||
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
|
||||
verify-healing: ## verify healing and replacing disks with minio binary
|
||||
@echo "Verify healing build with race"
|
||||
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||
@(env bash $(PWD)/buildscripts/unaligned-healing.sh)
|
||||
|
||||
verify-healing-inconsistent-versions: ## verify resolving inconsistent versions
|
||||
@echo "Verify resolving inconsistent versions build with race"
|
||||
@CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
|
||||
|
||||
build: checks ## builds minio to $(PWD)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# MinIO FIPS Builds
|
||||
|
||||
MinIO creates FIPS builds using a patched version of the Go compiler (that uses BoringCrypto, from BoringSSL, which is [FIPS 140-2 validated](https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2964.pdf)) published by the Golang Team [here](https://github.com/golang/go/tree/dev.boringcrypto/misc/boring).
|
||||
|
||||
MinIO FIPS executables are available at http://dl.min.io - they are only published for `linux-amd64` architecture as binary files with the suffix `.fips`. We also publish corresponding container images to our official image repositories.
|
||||
|
||||
We are not making any statements or representations about the suitability of this code or build in relation to the FIPS 140-2 standard. Interested users will have to evaluate for themselves whether this is useful for their own purposes.
|
||||
@@ -196,12 +196,6 @@ iptables -A INPUT -p tcp --dport 9000:9010 -j ACCEPT
|
||||
service iptables restart
|
||||
```
|
||||
|
||||
## Pre-existing data
|
||||
|
||||
When deployed on a single drive, MinIO server lets clients access any pre-existing data in the data directory. For example, if MinIO is started with the command `minio server /mnt/data`, any pre-existing data in the `/mnt/data` directory would be accessible to the clients.
|
||||
|
||||
The above statement is also valid for all gateway backends.
|
||||
|
||||
## Test MinIO Connectivity
|
||||
|
||||
### Test using MinIO Console
|
||||
@@ -244,13 +238,13 @@ mc admin update <minio alias, e.g., myminio>
|
||||
|
||||
- For deployments without external internet access (e.g. airgapped environments), download the binary from <https://dl.min.io> and replace the existing MinIO binary let's say for example `/opt/bin/minio`, apply executable permissions `chmod +x /opt/bin/minio` and do `mc admin service restart alias/`.
|
||||
|
||||
- For RPM/DEB installations, upgrade packages **parallelly** on all servers. Once upgraded, perform `systemctl restart minio` across all nodes in **parallel**. RPM/DEB based installations are usually automated using [`ansible`](https://github.com/minio/ansible-minio).
|
||||
- For installations using Systemd MinIO service, upgrade via RPM/DEB packages **parallelly** on all servers or replace the binary lets say `/opt/bin/minio` on all nodes, apply executable permissions `chmod +x /opt/bin/minio`. Proceed to perform `systemctl restart minio` across all nodes in **parallel**.
|
||||
|
||||
### Upgrade Checklist
|
||||
|
||||
- Test all upgrades in a lower environment (DEV, QA, UAT) before applying to production. Performing blind upgrades in production environments carries significant risk.
|
||||
- Read the release notes for the targeted MinIO release *before* performing any installation, there is no forced requirement to upgrade to latest releases every week. If it has a bug fix you are looking for then yes, else avoid actively upgrading a running production system.
|
||||
- Make sure MinIO process has write access to `/opt/bin` if you plan to use `mc admin update`. This is needed for MinIO to download the latest binary from <https://dl.min.io> and save it locally for upgrades.
|
||||
- If you plan to use `mc admin update`, MinIO process must have write access to the parent directory to provide in-place upgrades.
|
||||
- `mc admin update` is not supported in kubernetes/container environments, container environments provide their own mechanisms for container updates.
|
||||
- **We do not recommend upgrading one MinIO server at a time, the product is designed to support parallel upgrades please follow our recommended guidelines.**
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
export GORACE="history_size=7"
|
||||
## TODO remove `dsync` from race detector once this is merged and released https://go-review.googlesource.com/c/go/+/333529/
|
||||
for d in $(go list ./... | grep -v dsync); do
|
||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||
|
||||
@@ -145,7 +145,7 @@ func (a adminAPIHandlers) SetRemoteTargetHandler(w http.ResponseWriter, r *http.
|
||||
bucket := pathClean(vars["bucket"])
|
||||
update := r.Form.Get("update") == "true"
|
||||
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -274,7 +274,8 @@ func (a adminAPIHandlers) ListRemoteTargetsHandler(w http.ResponseWriter, r *htt
|
||||
vars := mux.Vars(r)
|
||||
bucket := pathClean(vars["bucket"])
|
||||
arnType := vars["type"]
|
||||
if !globalIsErasure {
|
||||
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -314,7 +315,7 @@ func (a adminAPIHandlers) RemoveRemoteTargetHandler(w http.ResponseWriter, r *ht
|
||||
bucket := pathClean(vars["bucket"])
|
||||
arn := vars["arn"]
|
||||
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierBackendInUse",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errTierBackendNotEmpty):
|
||||
apiErr = APIError{
|
||||
|
||||
@@ -33,7 +33,8 @@ import (
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
"github.com/minio/minio/internal/config/policy/opa"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
@@ -104,7 +105,7 @@ func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config,
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
globalNotificationSys.SignalService(serviceReloadDynamic)
|
||||
globalNotificationSys.SignalConfigReload(subSys)
|
||||
// Tell the client that dynamic config was applied.
|
||||
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
||||
}
|
||||
@@ -436,14 +437,16 @@ func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Reques
|
||||
off = !cache.Enabled(kv)
|
||||
case config.StorageClassSubSys:
|
||||
off = !storageclass.Enabled(kv)
|
||||
case config.PolicyOPASubSys:
|
||||
off = !opa.Enabled(kv)
|
||||
case config.PolicyPluginSubSys:
|
||||
off = !polplugin.Enabled(kv)
|
||||
case config.IdentityOpenIDSubSys:
|
||||
off = !openid.Enabled(kv)
|
||||
case config.IdentityLDAPSubSys:
|
||||
off = !xldap.Enabled(kv)
|
||||
case config.IdentityTLSSubSys:
|
||||
off = !globalSTSTLSConfig.Enabled
|
||||
case config.IdentityPluginSubSys:
|
||||
off = !idplugin.Enabled(kv)
|
||||
}
|
||||
if off {
|
||||
s.WriteString(config.KvComment)
|
||||
|
||||
@@ -241,6 +241,8 @@ func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *ht
|
||||
return
|
||||
}
|
||||
}
|
||||
case madmin.SRBucketMetaTypeVersionConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning)
|
||||
case madmin.SRBucketMetaTypeTags:
|
||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags)
|
||||
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||
|
||||
@@ -26,6 +26,7 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -41,11 +42,15 @@ func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
||||
}
|
||||
|
||||
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
t.Skip("windows is clunky")
|
||||
}
|
||||
|
||||
baseTestCases := []TestSuiteCommon{
|
||||
// Init and run test on FS backend with signature v4.
|
||||
{serverType: "FS", signer: signerV4},
|
||||
// Init and run test on FS backend, with tls enabled.
|
||||
{serverType: "FS", signer: signerV4, secure: true},
|
||||
// Init and run test on ErasureSD backend with signature v4.
|
||||
{serverType: "ErasureSD", signer: signerV4},
|
||||
// Init and run test on ErasureSD backend, with tls enabled.
|
||||
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||
// Init and run test on Erasure backend.
|
||||
{serverType: "Erasure", signer: signerV4},
|
||||
// Init and run test on ErasureSet backend.
|
||||
|
||||
+71
-35
@@ -241,6 +241,15 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
||||
if updReq.IsRemove {
|
||||
err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
||||
} else {
|
||||
// Check if group already exists
|
||||
if _, gerr := globalIAMSys.GetGroupDescription(updReq.Group); gerr != nil {
|
||||
// If group does not exist, then check if the group has beginning and end space characters
|
||||
// we will reject such group names.
|
||||
if errors.Is(gerr, errNoSuchGroup) && hasSpaceBE(updReq.Group) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
err = globalIAMSys.AddUsersToGroup(ctx, updReq.Group, updReq.Members)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -442,6 +451,12 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
||||
if !exists && hasSpaceBE(accessKey) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
checkDenyOnly := false
|
||||
if accessKey == cred.AccessKey {
|
||||
// Check that there is no explicit deny - otherwise it's allowed
|
||||
@@ -533,6 +548,12 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// service account access key cannot have space characters beginning and end of the string.
|
||||
if hasSpaceBE(createReq.AccessKey) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
targetUser string
|
||||
targetGroups []string
|
||||
@@ -656,29 +677,6 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Call hook for cluster-replication if the service account is not for a
|
||||
// root user.
|
||||
if newCred.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Create: &madmin.SRSvcAccCreate{
|
||||
Parent: newCred.ParentUser,
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
Groups: newCred.Groups,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: createReq.Policy,
|
||||
Status: auth.AccountOn,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
createResp := madmin.AddServiceAccountResp{
|
||||
Credentials: madmin.Credentials{
|
||||
AccessKey: newCred.AccessKey,
|
||||
@@ -699,6 +697,29 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
|
||||
// Call hook for cluster-replication if the service account is not for a
|
||||
// root user.
|
||||
if newCred.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Create: &madmin.SRSvcAccCreate{
|
||||
Parent: newCred.ParentUser,
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
Groups: newCred.Groups,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: createReq.Policy,
|
||||
Status: auth.AccountOn,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateServiceAccount - POST /minio/admin/v3/update-service-account
|
||||
@@ -993,11 +1014,9 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
svcAccount, _, err := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// We do not care if service account is readable or not at this point,
|
||||
// since this is a delete call we shall allow it to be deleted if possible.
|
||||
svcAccount, _, _ := globalIAMSys.GetServiceAccount(ctx, serviceAccount)
|
||||
|
||||
adminPrivilege := globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
@@ -1012,7 +1031,7 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
if cred.ParentUser != "" {
|
||||
parentUser = cred.ParentUser
|
||||
}
|
||||
if parentUser != svcAccount.ParentUser {
|
||||
if svcAccount.ParentUser != "" && parentUser != svcAccount.ParentUser {
|
||||
// The service account belongs to another user but return not
|
||||
// found error to mitigate brute force attacks. or the
|
||||
// serviceAccount doesn't exist.
|
||||
@@ -1021,23 +1040,21 @@ func (a adminAPIHandlers) DeleteServiceAccount(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
}
|
||||
|
||||
err = globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true)
|
||||
if err != nil {
|
||||
if err := globalIAMSys.DeleteServiceAccount(ctx, serviceAccount, true); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook - non-root user accounts are replicated.
|
||||
if svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
err = globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
if svcAccount.ParentUser != "" && svcAccount.ParentUser != globalActiveCred.AccessKey {
|
||||
if err := globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Delete: &madmin.SRSvcAccDelete{
|
||||
AccessKey: serviceAccount,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1088,6 +1105,19 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
||||
rd = true
|
||||
}
|
||||
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: iampolicy.GetBucketLocationAction,
|
||||
BucketName: bucketName,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: claims,
|
||||
}) {
|
||||
rd = true
|
||||
}
|
||||
|
||||
if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
@@ -1384,6 +1414,12 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
||||
vars := mux.Vars(r)
|
||||
policyName := vars["name"]
|
||||
|
||||
// Policy has space characters in begin and end reject such inputs.
|
||||
if hasSpaceBE(policyName) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Error out if Content-Length is missing.
|
||||
if r.ContentLength <= 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMissingContentLength), r.URL)
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -97,6 +98,29 @@ func (s *TestSuiteIAM) iamSetup(c *check) {
|
||||
}
|
||||
}
|
||||
|
||||
// List of all IAM test suites (i.e. test server configuration combinations)
|
||||
// common to tests.
|
||||
var iamTestSuites = func() []*TestSuiteIAM {
|
||||
baseTestCases := []TestSuiteCommon{
|
||||
// Init and run test on ErasureSD backend with signature v4.
|
||||
{serverType: "ErasureSD", signer: signerV4},
|
||||
// Init and run test on ErasureSD backend, with tls enabled.
|
||||
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||
// Init and run test on Erasure backend.
|
||||
{serverType: "Erasure", signer: signerV4},
|
||||
// Init and run test on ErasureSet backend.
|
||||
{serverType: "ErasureSet", signer: signerV4},
|
||||
}
|
||||
testCases := []*TestSuiteIAM{}
|
||||
for _, bt := range baseTestCases {
|
||||
testCases = append(testCases,
|
||||
newTestSuiteIAM(bt, false),
|
||||
newTestSuiteIAM(bt, true),
|
||||
)
|
||||
}
|
||||
return testCases
|
||||
}()
|
||||
|
||||
const (
|
||||
EnvTestEtcdBackend = "ETCD_SERVER"
|
||||
)
|
||||
@@ -166,30 +190,12 @@ func (s *TestSuiteIAM) getUserClient(c *check, accessKey, secretKey, sessionToke
|
||||
}
|
||||
|
||||
func TestIAMInternalIDPServerSuite(t *testing.T) {
|
||||
baseTestCases := []TestSuiteCommon{
|
||||
// Init and run test on FS backend with signature v4.
|
||||
{serverType: "FS", signer: signerV4},
|
||||
// Init and run test on FS backend, with tls enabled.
|
||||
{serverType: "FS", signer: signerV4, secure: true},
|
||||
// Init and run test on Erasure backend.
|
||||
{serverType: "Erasure", signer: signerV4},
|
||||
// Init and run test on ErasureSet backend.
|
||||
{serverType: "ErasureSet", signer: signerV4},
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
t.Skip("windows is clunky disable these tests")
|
||||
}
|
||||
testCases := []*TestSuiteIAM{}
|
||||
for _, bt := range baseTestCases {
|
||||
testCases = append(testCases,
|
||||
newTestSuiteIAM(bt, false),
|
||||
newTestSuiteIAM(bt, true),
|
||||
)
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
etcdStr := ""
|
||||
if testCase.withEtcdBackend {
|
||||
etcdStr = " (with etcd backend)"
|
||||
}
|
||||
for i, testCase := range iamTestSuites {
|
||||
t.Run(
|
||||
fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
|
||||
fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
|
||||
func(t *testing.T) {
|
||||
suite := testCase
|
||||
c := &check{t, testCase.serverType}
|
||||
@@ -236,6 +242,7 @@ func (s *TestSuiteIAM) TestUserCreate(c *check) {
|
||||
if err != nil {
|
||||
c.Fatalf("unable to set policy: %v", err)
|
||||
}
|
||||
|
||||
client := s.getUserClient(c, accessKey, secretKey, "")
|
||||
err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
|
||||
if err != nil {
|
||||
@@ -973,6 +980,166 @@ func (s *TestSuiteIAM) TestServiceAccountOpsByAdmin(c *check) {
|
||||
c.assertSvcAccDeletion(ctx, s, s.adm, accessKey, bucket)
|
||||
}
|
||||
|
||||
func (s *TestSuiteIAM) SetUpAccMgmtPlugin(c *check) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
pluginEndpoint := os.Getenv("POLICY_PLUGIN_ENDPOINT")
|
||||
if pluginEndpoint == "" {
|
||||
c.Skip("POLICY_PLUGIN_ENDPOINT not given - skipping.")
|
||||
}
|
||||
|
||||
configCmds := []string{
|
||||
"policy_plugin",
|
||||
"url=" + pluginEndpoint,
|
||||
}
|
||||
|
||||
_, err := s.adm.SetConfigKV(ctx, strings.Join(configCmds, " "))
|
||||
if err != nil {
|
||||
c.Fatalf("unable to setup access management plugin for tests: %v", err)
|
||||
}
|
||||
|
||||
s.RestartIAMSuite(c)
|
||||
}
|
||||
|
||||
// TestIAM_AMPInternalIDPServerSuite - tests for access management plugin
|
||||
func TestIAM_AMPInternalIDPServerSuite(t *testing.T) {
|
||||
for i, testCase := range iamTestSuites {
|
||||
t.Run(
|
||||
fmt.Sprintf("Test: %d, ServerType: %s", i+1, testCase.ServerTypeDescription),
|
||||
func(t *testing.T) {
|
||||
suite := testCase
|
||||
c := &check{t, testCase.serverType}
|
||||
|
||||
suite.SetUpSuite(c)
|
||||
defer suite.TearDownSuite(c)
|
||||
|
||||
suite.SetUpAccMgmtPlugin(c)
|
||||
|
||||
suite.TestAccMgmtPlugin(c)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccMgmtPlugin - this test assumes that the access-management-plugin is
|
||||
// the same as the example in `docs/iam/access-manager-plugin.go` -
|
||||
// specifically, it denies only `s3:Put*` operations on non-root accounts.
|
||||
func (s *TestSuiteIAM) TestAccMgmtPlugin(c *check) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 0. Check that owner is able to make-bucket.
|
||||
bucket := getRandomBucketName()
|
||||
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
|
||||
if err != nil {
|
||||
c.Fatalf("bucket creat error: %v", err)
|
||||
}
|
||||
|
||||
// 1. Create a user.
|
||||
accessKey, secretKey := mustGenerateCredentials(c)
|
||||
err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||
if err != nil {
|
||||
c.Fatalf("Unable to set user: %v", err)
|
||||
}
|
||||
|
||||
// 2. Check new user appears in listing
|
||||
usersMap, err := s.adm.ListUsers(ctx)
|
||||
if err != nil {
|
||||
c.Fatalf("error listing: %v", err)
|
||||
}
|
||||
v, ok := usersMap[accessKey]
|
||||
if !ok {
|
||||
c.Fatalf("user not listed: %s", accessKey)
|
||||
}
|
||||
c.Assert(v.Status, madmin.AccountEnabled)
|
||||
|
||||
// 3. Check that user is able to make a bucket.
|
||||
client := s.getUserClient(c, accessKey, secretKey, "")
|
||||
err = client.MakeBucket(ctx, getRandomBucketName(), minio.MakeBucketOptions{})
|
||||
if err != nil {
|
||||
c.Fatalf("user not create bucket: %v", err)
|
||||
}
|
||||
|
||||
// 3.1 check user has access to bucket
|
||||
c.mustListObjects(ctx, client, bucket)
|
||||
|
||||
// 3.2 check that user cannot upload an object.
|
||||
_, err = client.PutObject(ctx, bucket, "objectName", bytes.NewBuffer([]byte("some content")), 12, minio.PutObjectOptions{})
|
||||
if err == nil {
|
||||
c.Fatalf("user was able to upload unexpectedly")
|
||||
}
|
||||
|
||||
// Create an madmin client with user creds
|
||||
userAdmClient, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
|
||||
Creds: cr.NewStaticV4(accessKey, secretKey, ""),
|
||||
Secure: s.secure,
|
||||
})
|
||||
if err != nil {
|
||||
c.Fatalf("Err creating user admin client: %v", err)
|
||||
}
|
||||
userAdmClient.SetCustomTransport(s.TestSuiteCommon.client.Transport)
|
||||
|
||||
// Create svc acc
|
||||
cr := c.mustCreateSvcAccount(ctx, accessKey, userAdmClient)
|
||||
|
||||
// 1. Check that svc account appears in listing
|
||||
c.assertSvcAccAppearsInListing(ctx, userAdmClient, accessKey, cr.AccessKey)
|
||||
|
||||
// 2. Check that svc account info can be queried
|
||||
c.assertSvcAccInfoQueryable(ctx, userAdmClient, accessKey, cr.AccessKey, false)
|
||||
|
||||
// 3. Check S3 access
|
||||
c.assertSvcAccS3Access(ctx, s, cr, bucket)
|
||||
|
||||
// Check that session policies do not apply - as policy enforcement is
|
||||
// delegated to plugin.
|
||||
{
|
||||
svcAK, svcSK := mustGenerateCredentials(c)
|
||||
|
||||
// This policy does not allow listing objects.
|
||||
policyBytes := []byte(fmt.Sprintf(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::%s/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, bucket))
|
||||
cr, err := userAdmClient.AddServiceAccount(ctx, madmin.AddServiceAccountReq{
|
||||
Policy: policyBytes,
|
||||
TargetUser: accessKey,
|
||||
AccessKey: svcAK,
|
||||
SecretKey: svcSK,
|
||||
})
|
||||
if err != nil {
|
||||
c.Fatalf("Unable to create svc acc: %v", err)
|
||||
}
|
||||
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
|
||||
// Though the attached policy does not allow listing, it will be
|
||||
// ignored because the plugin allows it.
|
||||
c.mustListObjects(ctx, svcClient, bucket)
|
||||
}
|
||||
|
||||
// 4. Check that service account's secret key and account status can be
|
||||
// updated.
|
||||
c.assertSvcAccSecretKeyAndStatusUpdate(ctx, s, userAdmClient, accessKey, bucket)
|
||||
|
||||
// 5. Check that service account can be deleted.
|
||||
c.assertSvcAccDeletion(ctx, s, userAdmClient, accessKey, bucket)
|
||||
|
||||
// 6. Check that service account **can** be created for some other user.
|
||||
// This is possible because of the policy enforced in the plugin.
|
||||
c.mustCreateSvcAccount(ctx, globalActiveCred.AccessKey, userAdmClient)
|
||||
}
|
||||
|
||||
func (c *check) mustCreateIAMUser(ctx context.Context, admClnt *madmin.AdminClient) madmin.Credentials {
|
||||
randUser := mustGetUUID()
|
||||
randPass := mustGetUUID()
|
||||
@@ -1026,7 +1193,7 @@ func (c *check) mustNotListObjects(ctx context.Context, client *minio.Client, bu
|
||||
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
||||
v, ok := <-res
|
||||
if !ok || v.Err == nil {
|
||||
c.Fatalf("user was able to list unexpectedly!")
|
||||
c.Fatalf("user was able to list unexpectedly! on %s", bucket)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1034,11 +1201,20 @@ func (c *check) mustListObjects(ctx context.Context, client *minio.Client, bucke
|
||||
res := client.ListObjects(ctx, bucket, minio.ListObjectsOptions{})
|
||||
v, ok := <-res
|
||||
if ok && v.Err != nil {
|
||||
msg := fmt.Sprintf("user was unable to list: %v", v.Err)
|
||||
c.Fatalf(msg)
|
||||
c.Fatalf("user was unable to list: %v", v.Err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *check) mustNotUpload(ctx context.Context, client *minio.Client, bucket string) {
|
||||
_, err := client.PutObject(ctx, bucket, "some-object", bytes.NewBuffer([]byte("stuff")), 5, minio.PutObjectOptions{})
|
||||
if e, ok := err.(minio.ErrorResponse); ok {
|
||||
if e.Code == "AccessDenied" {
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Fatalf("upload did not get an AccessDenied error - got %#v instead", err)
|
||||
}
|
||||
|
||||
func (c *check) assertSvcAccS3Access(ctx context.Context, s *TestSuiteIAM, cr madmin.Credentials, bucket string) {
|
||||
svcClient := s.getUserClient(c, cr.AccessKey, cr.SecretKey, "")
|
||||
c.mustListObjects(ctx, svcClient, bucket)
|
||||
|
||||
+85
-35
@@ -57,7 +57,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxEConfigJSONSize = 262272
|
||||
maxEConfigJSONSize = 262272
|
||||
kubernetesVersionEndpoint = "https://kubernetes.default.svc/version"
|
||||
)
|
||||
|
||||
// Only valid query params for mgmt admin APIs.
|
||||
@@ -653,12 +654,10 @@ func (a adminAPIHandlers) ProfileHandler(w http.ResponseWriter, r *http.Request)
|
||||
globalProfilerMu.Unlock()
|
||||
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
for k, v := range globalProfiler {
|
||||
v.Stop()
|
||||
delete(globalProfiler, k)
|
||||
@@ -806,8 +805,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this setup has an erasure coded backend.
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -999,7 +997,7 @@ func (a adminAPIHandlers) BackgroundHealStatusHandler(w http.ResponseWriter, r *
|
||||
}
|
||||
|
||||
// Check if this setup has an erasure coded backend.
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrHealNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1079,7 +1077,7 @@ func (a adminAPIHandlers) ObjectSpeedtestHandler(w http.ResponseWriter, r *http.
|
||||
return
|
||||
}
|
||||
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1229,7 +1227,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
||||
return
|
||||
}
|
||||
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1411,9 +1409,15 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
peers, _ := newPeerRestClients(globalEndpoints)
|
||||
|
||||
globalTrace.Subscribe(traceCh, ctx.Done(), func(entry interface{}) bool {
|
||||
traceFn := func(entry interface{}) bool {
|
||||
return mustTrace(entry, traceOpts)
|
||||
})
|
||||
}
|
||||
|
||||
err = globalTrace.Subscribe(traceCh, ctx.Done(), traceFn)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
@@ -1485,7 +1489,11 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
peers, _ := newPeerRestClients(globalEndpoints)
|
||||
|
||||
globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
||||
err = globalConsoleSys.Subscribe(logCh, ctx.Done(), node, limitLines, logKind, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for _, peer := range peers {
|
||||
if peer == nil {
|
||||
@@ -1768,6 +1776,36 @@ func getServerInfo(ctx context.Context, r *http.Request) madmin.InfoMessage {
|
||||
}
|
||||
}
|
||||
|
||||
func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
|
||||
ctx, cancel := context.WithCancel(dctx)
|
||||
defer cancel()
|
||||
|
||||
ki := madmin.KubernetesInfo{}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, kubernetesVersionEndpoint, nil)
|
||||
if err != nil {
|
||||
ki.Error = err.Error()
|
||||
return ki
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: NewGatewayHTTPTransport(),
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
ki.Error = err.Error()
|
||||
return ki
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
if err := decoder.Decode(&ki); err != nil {
|
||||
ki.Error = err.Error()
|
||||
}
|
||||
return ki
|
||||
}
|
||||
|
||||
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
|
||||
// ----------
|
||||
// Get server health info
|
||||
@@ -1793,9 +1831,6 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
healthInfoCh := make(chan madmin.HealthInfo)
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
partialWrite := func(oinfo madmin.HealthInfo) {
|
||||
healthInfoCh <- oinfo
|
||||
}
|
||||
|
||||
setCommonHeaders(w)
|
||||
|
||||
@@ -1826,9 +1861,6 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
deadlinedCtx, deadlineCancel := context.WithTimeout(ctx, deadline)
|
||||
defer deadlineCancel()
|
||||
|
||||
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
|
||||
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
|
||||
if err != nil { // returns a locked lock
|
||||
@@ -1837,6 +1869,9 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
defer nsLock.Unlock(lkctx.Cancel)
|
||||
|
||||
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
|
||||
defer healthCancel()
|
||||
|
||||
hostAnonymizer := createHostAnonymizer()
|
||||
// anonAddr - Anonymizes hosts in given input string.
|
||||
anonAddr := func(addr string) string {
|
||||
@@ -1855,13 +1890,27 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
info.SetAddr(anonAddr(info.GetAddr()))
|
||||
}
|
||||
|
||||
partialWrite := func(oinfo madmin.HealthInfo) {
|
||||
select {
|
||||
case healthInfoCh <- oinfo:
|
||||
case <-healthCtx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
getAndWritePlatformInfo := func() {
|
||||
if IsKubernetes() {
|
||||
healthInfo.Sys.KubernetesInfo = getKubernetesInfo(healthCtx)
|
||||
partialWrite(healthInfo)
|
||||
}
|
||||
}
|
||||
|
||||
getAndWriteCPUs := func() {
|
||||
if query.Get("syscpu") == "true" {
|
||||
localCPUInfo := madmin.GetCPUs(deadlinedCtx, globalLocalNodeName)
|
||||
localCPUInfo := madmin.GetCPUs(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localCPUInfo)
|
||||
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, localCPUInfo)
|
||||
|
||||
peerCPUInfo := globalNotificationSys.GetCPUs(deadlinedCtx)
|
||||
peerCPUInfo := globalNotificationSys.GetCPUs(healthCtx)
|
||||
for _, cpuInfo := range peerCPUInfo {
|
||||
anonymizeAddr(&cpuInfo)
|
||||
healthInfo.Sys.CPUInfo = append(healthInfo.Sys.CPUInfo, cpuInfo)
|
||||
@@ -1873,11 +1922,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWritePartitions := func() {
|
||||
if query.Get("sysdrivehw") == "true" {
|
||||
localPartitions := madmin.GetPartitions(deadlinedCtx, globalLocalNodeName)
|
||||
localPartitions := madmin.GetPartitions(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localPartitions)
|
||||
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, localPartitions)
|
||||
|
||||
peerPartitions := globalNotificationSys.GetPartitions(deadlinedCtx)
|
||||
peerPartitions := globalNotificationSys.GetPartitions(healthCtx)
|
||||
for _, p := range peerPartitions {
|
||||
anonymizeAddr(&p)
|
||||
healthInfo.Sys.Partitions = append(healthInfo.Sys.Partitions, p)
|
||||
@@ -1888,11 +1937,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteOSInfo := func() {
|
||||
if query.Get("sysosinfo") == "true" {
|
||||
localOSInfo := madmin.GetOSInfo(deadlinedCtx, globalLocalNodeName)
|
||||
localOSInfo := madmin.GetOSInfo(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localOSInfo)
|
||||
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, localOSInfo)
|
||||
|
||||
peerOSInfos := globalNotificationSys.GetOSInfo(deadlinedCtx)
|
||||
peerOSInfos := globalNotificationSys.GetOSInfo(healthCtx)
|
||||
for _, o := range peerOSInfos {
|
||||
anonymizeAddr(&o)
|
||||
healthInfo.Sys.OSInfo = append(healthInfo.Sys.OSInfo, o)
|
||||
@@ -1903,11 +1952,11 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteMemInfo := func() {
|
||||
if query.Get("sysmem") == "true" {
|
||||
localMemInfo := madmin.GetMemInfo(deadlinedCtx, globalLocalNodeName)
|
||||
localMemInfo := madmin.GetMemInfo(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localMemInfo)
|
||||
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, localMemInfo)
|
||||
|
||||
peerMemInfos := globalNotificationSys.GetMemInfo(deadlinedCtx)
|
||||
peerMemInfos := globalNotificationSys.GetMemInfo(healthCtx)
|
||||
for _, m := range peerMemInfos {
|
||||
anonymizeAddr(&m)
|
||||
healthInfo.Sys.MemInfo = append(healthInfo.Sys.MemInfo, m)
|
||||
@@ -1918,12 +1967,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteSysErrors := func() {
|
||||
if query.Get(string(madmin.HealthDataTypeSysErrors)) == "true" {
|
||||
localSysErrors := madmin.GetSysErrors(deadlinedCtx, globalLocalNodeName)
|
||||
localSysErrors := madmin.GetSysErrors(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localSysErrors)
|
||||
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, localSysErrors)
|
||||
partialWrite(healthInfo)
|
||||
|
||||
peerSysErrs := globalNotificationSys.GetSysErrors(deadlinedCtx)
|
||||
peerSysErrs := globalNotificationSys.GetSysErrors(healthCtx)
|
||||
for _, se := range peerSysErrs {
|
||||
anonymizeAddr(&se)
|
||||
healthInfo.Sys.SysErrs = append(healthInfo.Sys.SysErrs, se)
|
||||
@@ -1934,12 +1983,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteSysConfig := func() {
|
||||
if query.Get(string(madmin.HealthDataTypeSysConfig)) == "true" {
|
||||
localSysConfig := madmin.GetSysConfig(deadlinedCtx, globalLocalNodeName)
|
||||
localSysConfig := madmin.GetSysConfig(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localSysConfig)
|
||||
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, localSysConfig)
|
||||
partialWrite(healthInfo)
|
||||
|
||||
peerSysConfig := globalNotificationSys.GetSysConfig(deadlinedCtx)
|
||||
peerSysConfig := globalNotificationSys.GetSysConfig(healthCtx)
|
||||
for _, sc := range peerSysConfig {
|
||||
anonymizeAddr(&sc)
|
||||
healthInfo.Sys.SysConfig = append(healthInfo.Sys.SysConfig, sc)
|
||||
@@ -1950,12 +1999,12 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteSysServices := func() {
|
||||
if query.Get(string(madmin.HealthDataTypeSysServices)) == "true" {
|
||||
localSysServices := madmin.GetSysServices(deadlinedCtx, globalLocalNodeName)
|
||||
localSysServices := madmin.GetSysServices(healthCtx, globalLocalNodeName)
|
||||
anonymizeAddr(&localSysServices)
|
||||
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, localSysServices)
|
||||
partialWrite(healthInfo)
|
||||
|
||||
peerSysServices := globalNotificationSys.GetSysServices(deadlinedCtx)
|
||||
peerSysServices := globalNotificationSys.GetSysServices(healthCtx)
|
||||
for _, ss := range peerSysServices {
|
||||
anonymizeAddr(&ss)
|
||||
healthInfo.Sys.SysServices = append(healthInfo.Sys.SysServices, ss)
|
||||
@@ -2030,10 +2079,10 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
getAndWriteProcInfo := func() {
|
||||
if query.Get("sysprocess") == "true" {
|
||||
localProcInfo := madmin.GetProcInfo(deadlinedCtx, globalLocalNodeName)
|
||||
localProcInfo := madmin.GetProcInfo(healthCtx, globalLocalNodeName)
|
||||
anonymizeProcInfo(&localProcInfo)
|
||||
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, localProcInfo)
|
||||
peerProcInfos := globalNotificationSys.GetProcInfo(deadlinedCtx)
|
||||
peerProcInfos := globalNotificationSys.GetProcInfo(healthCtx)
|
||||
for _, p := range peerProcInfos {
|
||||
anonymizeProcInfo(&p)
|
||||
healthInfo.Sys.ProcInfo = append(healthInfo.Sys.ProcInfo, p)
|
||||
@@ -2177,6 +2226,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
defer close(healthInfoCh)
|
||||
|
||||
partialWrite(healthInfo) // Write first message with only version and deployment id populated
|
||||
getAndWritePlatformInfo()
|
||||
getAndWriteCPUs()
|
||||
getAndWritePartitions()
|
||||
getAndWriteOSInfo()
|
||||
@@ -2258,7 +2308,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
case <-deadlinedCtx.Done():
|
||||
case <-healthCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,6 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-periodicTimer.C:
|
||||
periodicTimer.Reset(time.Minute * 5)
|
||||
now := UTCNow()
|
||||
ahs.Lock()
|
||||
for path, h := range ahs.healSeqMap {
|
||||
@@ -203,6 +202,8 @@ func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
ahs.Unlock()
|
||||
|
||||
periodicTimer.Reset(time.Minute * 5)
|
||||
case <-ctx.Done():
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
@@ -581,12 +582,7 @@ func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
// heal-results in memory and the client has not consumed it
|
||||
// for too long.
|
||||
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
||||
defer func() {
|
||||
// stop the timeout timer so it is garbage collected.
|
||||
if !unconsumedTimer.Stop() {
|
||||
<-unconsumedTimer.C
|
||||
}
|
||||
}()
|
||||
defer unconsumedTimer.Stop()
|
||||
|
||||
var itemsLen int
|
||||
for {
|
||||
|
||||
+38
-40
@@ -170,50 +170,48 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
// Set Group Status
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SetGroupStatus))).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
if globalIsDistErasure || globalIsErasure {
|
||||
// GetBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||
// PutBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||
// GetBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.GetBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||
// PutBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.PutBucketQuotaConfigHandler))).Queries("bucket", "{bucket:.*}")
|
||||
|
||||
// Bucket replication operations
|
||||
// GetBucketTargetHandler
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
|
||||
// SetRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
|
||||
// RemoveRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||
// Bucket replication operations
|
||||
// GetBucketTargetHandler
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.ListRemoteTargetsHandler))).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
|
||||
// SetRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.SetRemoteTargetHandler))).Queries("bucket", "{bucket:.*}")
|
||||
// RemoveRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||
gz(httpTraceHdrs(adminAPI.RemoveRemoteTargetHandler))).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||
|
||||
// Remote Tier management operations
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
|
||||
// Tier stats
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
|
||||
// Remote Tier management operations
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.AddTierHandler)))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.EditTierHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(gz(httpTraceHdrs(adminAPI.ListTierHandler)))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.RemoveTierHandler)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(gz(httpTraceHdrs(adminAPI.VerifyTierHandler)))
|
||||
// Tier stats
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(gz(httpTraceHdrs(adminAPI.TierStatsHandler)))
|
||||
|
||||
// Cluster Replication APIs
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationRemove)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
|
||||
// Cluster Replication APIs
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationAdd)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationRemove)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationInfo)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationMetaInfo)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationStatus)))
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
||||
}
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerJoin)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerBucketOps))).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateIAMItem)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerReplicateBucketItem)))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerGetIDPSettings)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SiteReplicationEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerEdit)))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(gz(httpTraceHdrs(adminAPI.SRPeerRemove)))
|
||||
|
||||
if globalIsDistErasure {
|
||||
// Top locks
|
||||
|
||||
@@ -31,7 +31,10 @@ import (
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties {
|
||||
var localEndpoints Endpoints
|
||||
addr := r.Host
|
||||
addr := globalLocalNodeName
|
||||
if r != nil {
|
||||
addr = r.Host
|
||||
}
|
||||
if globalIsDistErasure {
|
||||
addr = globalLocalNodeName
|
||||
}
|
||||
@@ -40,7 +43,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
nodeName := endpoint.Host
|
||||
if nodeName == "" {
|
||||
nodeName = r.Host
|
||||
nodeName = addr
|
||||
}
|
||||
if endpoint.IsLocal {
|
||||
// Only proceed for local endpoints
|
||||
|
||||
+18
-7
@@ -131,7 +131,7 @@ const (
|
||||
ErrReplicationNeedsVersioningError
|
||||
ErrReplicationBucketNeedsVersioningError
|
||||
ErrReplicationDenyEditError
|
||||
ErrReplicationNoMatchingRuleError
|
||||
ErrReplicationNoExistingObjects
|
||||
ErrObjectRestoreAlreadyInProgress
|
||||
ErrNoSuchKey
|
||||
ErrNoSuchUpload
|
||||
@@ -280,6 +280,7 @@ const (
|
||||
ErrSiteReplicationBucketConfigError
|
||||
ErrSiteReplicationBucketMetaError
|
||||
ErrSiteReplicationIAMError
|
||||
ErrSiteReplicationConfigMissing
|
||||
|
||||
// Bucket Quota error codes
|
||||
ErrAdminBucketQuotaExceeded
|
||||
@@ -383,6 +384,7 @@ const (
|
||||
ErrAdminProfilerNotEnabled
|
||||
ErrInvalidDecompressedSize
|
||||
ErrAddUserInvalidArgument
|
||||
ErrAdminResourceInvalidArgument
|
||||
ErrAdminAccountNotEligible
|
||||
ErrAccountNotEligible
|
||||
ErrAdminServiceAccountNotFound
|
||||
@@ -891,15 +893,15 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Bandwidth limit for remote target must be atleast 100MBps",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrReplicationNoMatchingRuleError: {
|
||||
Code: "XMinioReplicationNoMatchingRule",
|
||||
Description: "No matching replication rule found for this object prefix",
|
||||
ErrReplicationNoExistingObjects: {
|
||||
Code: "XMinioReplicationNoExistingObjects",
|
||||
Description: "No matching ExistingsObjects rule enabled",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrReplicationDenyEditError: {
|
||||
Code: "XMinioReplicationDenyEdit",
|
||||
Description: "Cannot alter local replication config since this server is in a cluster replication setup",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBucketRemoteIdenticalToSource: {
|
||||
Code: "XMinioAdminRemoteIdenticalToSource",
|
||||
@@ -1165,7 +1167,7 @@ var errorCodes = errorCodeMap{
|
||||
ErrObjectExistsAsDirectory: {
|
||||
Code: "XMinioObjectExistsAsDirectory",
|
||||
Description: "Object name already exists as a directory.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidObjectName: {
|
||||
Code: "XMinioInvalidObjectName",
|
||||
@@ -1339,7 +1341,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "Error while replicating an IAM item",
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
},
|
||||
|
||||
ErrSiteReplicationConfigMissing: {
|
||||
Code: "XMinioSiteReplicationConfigMissingError",
|
||||
Description: "Site not found in site replication configuration",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMaximumExpires: {
|
||||
Code: "AuthorizationQueryParametersError",
|
||||
Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",
|
||||
@@ -1825,6 +1831,11 @@ var errorCodes = errorCodeMap{
|
||||
Description: "User is not allowed to be same as admin access key",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrAdminResourceInvalidArgument: {
|
||||
Code: "XMinioInvalidResource",
|
||||
Description: "Policy, user or group names are not allowed to begin or end with space characters",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrAdminAccountNotEligible: {
|
||||
Code: "XMinioInvalidIAMCredentials",
|
||||
Description: "The administrator key is not eligible for this operation",
|
||||
|
||||
@@ -728,6 +728,14 @@ func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, err
|
||||
}
|
||||
|
||||
func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) {
|
||||
if statusCode == 0 {
|
||||
statusCode = 200
|
||||
}
|
||||
// Similar check to http.checkWriteHeaderCode
|
||||
if statusCode < 100 || statusCode > 999 {
|
||||
logger.Error(fmt.Sprintf("invalid WriteHeader code %v", statusCode))
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
setCommonHeaders(w)
|
||||
if mType != mimeNone {
|
||||
w.Header().Set(xhttp.ContentType, string(mType))
|
||||
@@ -791,6 +799,12 @@ func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError
|
||||
err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalSite.Region)
|
||||
}
|
||||
|
||||
// Similar check to http.checkWriteHeaderCode
|
||||
if err.HTTPStatusCode < 100 || err.HTTPStatusCode > 999 {
|
||||
logger.Error(fmt.Sprintf("invalid WriteHeader code %v from %v", err.HTTPStatusCode, err.Code))
|
||||
err.HTTPStatusCode = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// Generate error response.
|
||||
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
|
||||
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
|
||||
|
||||
+2
-2
@@ -342,7 +342,7 @@ func registerAPIRouter(router *mux.Router) {
|
||||
collectAPIStats("getbucketnotification", maxClients(gz(httpTraceAll(api.GetBucketNotificationHandler))))).Queries("notification", "")
|
||||
// ListenNotification
|
||||
router.Methods(http.MethodGet).HandlerFunc(
|
||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
||||
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||
// ResetBucketReplicationStatus - MinIO extension API
|
||||
router.Methods(http.MethodGet).HandlerFunc(
|
||||
collectAPIStats("resetbucketreplicationstatus", maxClients(gz(httpTraceAll(api.ResetBucketReplicationStatusHandler))))).Queries("replication-reset-status", "")
|
||||
@@ -474,7 +474,7 @@ func registerAPIRouter(router *mux.Router) {
|
||||
|
||||
// ListenNotification
|
||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||
collectAPIStats("listennotification", maxClients(gz(httpTraceAll(api.ListenNotificationHandler))))).Queries("events", "{events:.*}")
|
||||
collectAPIStats("listennotification", gz(httpTraceAll(api.ListenNotificationHandler)))).Queries("events", "{events:.*}")
|
||||
|
||||
// ListBuckets
|
||||
apiRouter.Methods(http.MethodGet).Path(SlashSeparator).HandlerFunc(
|
||||
|
||||
+106
-104
File diff suppressed because one or more lines are too long
+3
-3
@@ -217,8 +217,8 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{},
|
||||
}
|
||||
}
|
||||
|
||||
// If OPA is set, return without any further checks.
|
||||
if globalPolicyOPA != nil {
|
||||
// If AuthZPlugin is set, return without any further checks.
|
||||
if newGlobalAuthZPluginFn() != nil {
|
||||
return claims.Map(), nil
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ func getClaimsFromTokenWithSecret(token, secret string) (map[string]interface{},
|
||||
logger.LogIf(GlobalContext, err, logger.Application)
|
||||
return nil, errAuthentication
|
||||
}
|
||||
claims.MapClaims[iampolicy.SessionPolicyName] = string(spBytes)
|
||||
claims.MapClaims[sessionPolicyNameExtracted] = string(spBytes)
|
||||
}
|
||||
|
||||
return claims.Map(), nil
|
||||
|
||||
@@ -312,9 +312,6 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-diskCheckTimer.C:
|
||||
// Reset to next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
|
||||
var erasureSetInPoolDisksToHeal []map[int][]StorageAPI
|
||||
|
||||
healDisks := globalBackgroundHealState.getHealLocalDiskEndpoints()
|
||||
@@ -448,6 +445,9 @@ func monitorLocalDisksAndHeal(ctx context.Context, z *erasureServerPools, bgSeq
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Reset for next interval.
|
||||
diskCheckTimer.Reset(defaultMonitorNewDiskInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -27,6 +26,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/minio/highwayhash"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
"golang.org/x/crypto/blake2b"
|
||||
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
|
||||
+26
-401
@@ -21,7 +21,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -33,7 +32,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -364,6 +362,18 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
||||
}) {
|
||||
bucketsInfo[n] = bucketInfo
|
||||
n++
|
||||
} else if globalIAMSys.IsAllowed(iampolicy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: iampolicy.GetBucketLocationAction,
|
||||
BucketName: bucketInfo.Name,
|
||||
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
|
||||
IsOwner: owner,
|
||||
ObjectName: "",
|
||||
Claims: cred.Claims,
|
||||
}) {
|
||||
bucketsInfo[n] = bucketInfo
|
||||
n++
|
||||
}
|
||||
}
|
||||
bucketsInfo = bucketsInfo[:n]
|
||||
@@ -471,9 +481,6 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
hasLockEnabled = true
|
||||
}
|
||||
|
||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
||||
suspended := globalBucketVersioningSys.Suspended(bucket)
|
||||
|
||||
type deleteResult struct {
|
||||
delInfo DeletedObject
|
||||
errInfo DeleteError
|
||||
@@ -481,8 +488,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
|
||||
deleteResults := make([]deleteResult, len(deleteObjectsReq.Objects))
|
||||
|
||||
vc, _ := globalBucketVersioningSys.Get(bucket)
|
||||
oss := make([]*objSweeper, len(deleteObjectsReq.Objects))
|
||||
|
||||
for index, object := range deleteObjectsReq.Objects {
|
||||
if apiErrCode := checkRequestAuthType(ctx, r, policy.DeleteObjectAction, bucket, object.ObjectName); apiErrCode != ErrNone {
|
||||
if apiErrCode == ErrSignatureDoesNotMatch || apiErrCode == ErrInvalidAccessKeyID {
|
||||
@@ -514,11 +521,11 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
|
||||
opts := ObjectOptions{
|
||||
VersionID: object.VersionID,
|
||||
Versioned: versioned,
|
||||
VersionSuspended: suspended,
|
||||
Versioned: vc.PrefixEnabled(object.ObjectName),
|
||||
VersionSuspended: vc.Suspended(),
|
||||
}
|
||||
|
||||
if replicateDeletes || object.VersionID != "" && hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||
if replicateDeletes || hasLockEnabled || !globalTierConfigMgr.Empty() {
|
||||
if !globalTierConfigMgr.Empty() && object.VersionID == "" && opts.VersionSuspended {
|
||||
opts.VersionID = nullVersionID
|
||||
}
|
||||
@@ -526,7 +533,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
if !globalTierConfigMgr.Empty() {
|
||||
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(versioned, suspended)
|
||||
oss[index] = newObjSweeper(bucket, object.ObjectName).WithVersion(opts.VersionID).WithVersioning(opts.Versioned, opts.VersionSuspended)
|
||||
oss[index].SetTransitionState(goi.TransitionedObject)
|
||||
}
|
||||
|
||||
@@ -547,7 +554,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
object.ReplicateDecisionStr = dsc.String()
|
||||
}
|
||||
}
|
||||
if object.VersionID != "" && hasLockEnabled {
|
||||
if hasLockEnabled {
|
||||
if apiErrCode := enforceRetentionBypassForDelete(ctx, r, bucket, object, goi, gerr); apiErrCode != ErrNone {
|
||||
apiErr := errorCodes.ToAPIErr(apiErrCode)
|
||||
deleteResults[index].errInfo = DeleteError{
|
||||
@@ -581,8 +588,8 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
|
||||
deleteList := toNames(objectsToDelete)
|
||||
dObjects, errs := deleteObjectsFn(ctx, bucket, deleteList, ObjectOptions{
|
||||
Versioned: versioned,
|
||||
VersionSuspended: suspended,
|
||||
PrefixEnabledFn: vc.PrefixEnabled,
|
||||
VersionSuspended: vc.Suspended(),
|
||||
})
|
||||
|
||||
for i := range errs {
|
||||
@@ -638,22 +645,12 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
||||
continue
|
||||
}
|
||||
|
||||
if replicateDeletes {
|
||||
if dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending {
|
||||
dv := DeletedObjectReplicationInfo{
|
||||
DeletedObject: dobj,
|
||||
Bucket: bucket,
|
||||
}
|
||||
scheduleReplicationDelete(ctx, dv, objectAPI)
|
||||
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
|
||||
dv := DeletedObjectReplicationInfo{
|
||||
DeletedObject: dobj,
|
||||
Bucket: bucket,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Notify deleted event for objects.
|
||||
for _, dobj := range deletedObjects {
|
||||
if dobj.ObjectName == "" {
|
||||
continue
|
||||
scheduleReplicationDelete(ctx, dv, objectAPI)
|
||||
}
|
||||
|
||||
eventName := event.ObjectRemovedDelete
|
||||
@@ -1365,7 +1362,7 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
@@ -1597,375 +1594,3 @@ func (api objectAPIHandlers) DeleteBucketTaggingHandler(w http.ResponseWriter, r
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
|
||||
// ----------
|
||||
// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
|
||||
func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketReplicationConfig")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
if !globalIsErasure {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if globalSiteReplicationSys.isEnabled() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
||||
return
|
||||
}
|
||||
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
|
||||
return
|
||||
}
|
||||
replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig)
|
||||
if apiErr != noError {
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
// Validate the received bucket replication config
|
||||
if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(replicationConfig)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
|
||||
// ----------
|
||||
// Gets the replication configuration for a bucket.
|
||||
func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketReplicationConfig")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, configData)
|
||||
}
|
||||
|
||||
// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
|
||||
// ----------
|
||||
func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteBucketReplicationConfig")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if globalSiteReplicationSys.isEnabled() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketReplicationMetricsHandler - GET Bucket replication metrics.
|
||||
// ----------
|
||||
// Gets the replication metrics for a bucket.
|
||||
func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketReplicationMetrics")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
var usageInfo BucketUsageInfo
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err == nil && !dataUsageInfo.LastUpdate.IsZero() {
|
||||
usageInfo = dataUsageInfo.BucketsUsage[bucket]
|
||||
}
|
||||
|
||||
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err = enc.Encode(getLatestReplicationStats(bucket, usageInfo)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
|
||||
// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
|
||||
// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
|
||||
// remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
|
||||
// already in progress it returns an error
|
||||
func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ResetBucketReplicationStart")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
durationStr := r.URL.Query().Get("older-than")
|
||||
arn := r.URL.Query().Get("arn")
|
||||
resetID := r.URL.Query().Get("reset-id")
|
||||
if resetID == "" {
|
||||
resetID = mustGetUUID()
|
||||
}
|
||||
var (
|
||||
days time.Duration
|
||||
err error
|
||||
)
|
||||
if durationStr != "" {
|
||||
days, err = time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
||||
}), r.URL)
|
||||
}
|
||||
}
|
||||
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if !config.HasActiveRules("", true) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoMatchingRuleError), r.URL)
|
||||
return
|
||||
}
|
||||
tgtArns := config.FilterTargetArns(
|
||||
replication.ObjectOpts{
|
||||
OpType: replication.ResyncReplicationType,
|
||||
TargetArn: arn,
|
||||
})
|
||||
|
||||
if len(tgtArns) == 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tgtArns) > 1 && arn == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("ARN should be specified for replication reset"),
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
var rinfo ResyncTargetsInfo
|
||||
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
|
||||
target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||
target.ResetID = resetID
|
||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
|
||||
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketRemoteConnectionErr:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
}
|
||||
if err := startReplicationResync(ctx, bucket, arn, resetID, resetBeforeDate, objectAPI); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: err,
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(rinfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, data)
|
||||
}
|
||||
|
||||
// ResetBucketReplicationStatusHandler - returns the status of replication reset.
|
||||
// This API is a MinIO only extension
|
||||
func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ResetBucketReplicationStatus")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
arn := r.URL.Query().Get("arn")
|
||||
var err error
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
globalReplicationPool.resyncState.RLock()
|
||||
brs, ok := globalReplicationPool.resyncState.statusMap[bucket]
|
||||
if !ok {
|
||||
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("No replication resync status available for %s", arn),
|
||||
}), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var rinfo ResyncTargetsInfo
|
||||
for tarn, st := range brs.TargetsMap {
|
||||
if arn != "" && tarn != arn {
|
||||
continue
|
||||
}
|
||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{
|
||||
Arn: tarn,
|
||||
ResetID: st.ResyncID,
|
||||
StartTime: st.StartTime,
|
||||
EndTime: st.EndTime,
|
||||
ResyncStatus: st.ResyncStatus.String(),
|
||||
ReplicatedSize: st.ReplicatedSize,
|
||||
ReplicatedCount: st.ReplicatedCount,
|
||||
FailedSize: st.FailedSize,
|
||||
FailedCount: st.FailedCount,
|
||||
Bucket: st.Bucket,
|
||||
Object: st.Object,
|
||||
})
|
||||
}
|
||||
globalReplicationPool.resyncState.RUnlock()
|
||||
data, err := json.Marshal(rinfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, data)
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ const (
|
||||
// 2. when a transitioned object expires (based on an ILM rule).
|
||||
func expireTransitionedObject(ctx context.Context, objectAPI ObjectLayer, oi *ObjectInfo, lcOpts lifecycle.ObjectOpts, action expireAction) error {
|
||||
var opts ObjectOptions
|
||||
opts.Versioned = globalBucketVersioningSys.Enabled(oi.Bucket)
|
||||
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name)
|
||||
opts.VersionID = lcOpts.VersionID
|
||||
opts.Expiration = ExpirationOptions{Expire: true}
|
||||
switch action {
|
||||
@@ -415,8 +415,8 @@ func transitionObject(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo)
|
||||
ETag: oi.ETag,
|
||||
},
|
||||
VersionID: oi.VersionID,
|
||||
Versioned: globalBucketVersioningSys.Enabled(oi.Bucket),
|
||||
VersionSuspended: globalBucketVersioningSys.Suspended(oi.Bucket),
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
|
||||
MTime: oi.ModTime,
|
||||
}
|
||||
return tier, objectAPI.TransitionObject(ctx, oi.Bucket, oi.Name, opts)
|
||||
@@ -575,8 +575,8 @@ func (r *RestoreObjectRequest) validate(ctx context.Context, objAPI ObjectLayer)
|
||||
|
||||
// postRestoreOpts returns ObjectOptions with version-id from the POST restore object request for a given bucket and object.
|
||||
func postRestoreOpts(ctx context.Context, r *http.Request, bucket, object string) (opts ObjectOptions, err error) {
|
||||
versioned := globalBucketVersioningSys.Enabled(bucket)
|
||||
versionSuspended := globalBucketVersioningSys.Suspended(bucket)
|
||||
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||
vid := strings.TrimSpace(r.Form.Get(xhttp.VersionID))
|
||||
if vid != "" && vid != nullVersionID {
|
||||
_, err := uuid.Parse(vid)
|
||||
@@ -627,8 +627,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
||||
meta[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES
|
||||
}
|
||||
return ObjectOptions{
|
||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||
UserDefined: meta,
|
||||
}
|
||||
}
|
||||
@@ -640,8 +640,8 @@ func putRestoreOpts(bucket, object string, rreq *RestoreObjectRequest, objInfo O
|
||||
}
|
||||
|
||||
return ObjectOptions{
|
||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, object),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, object),
|
||||
UserDefined: meta,
|
||||
VersionID: objInfo.VersionID,
|
||||
MTime: objInfo.ModTime,
|
||||
|
||||
@@ -203,7 +203,7 @@ func TestObjectIsRemote(t *testing.T) {
|
||||
if got := fi.IsRemote(); got != tc.remote {
|
||||
t.Fatalf("Test %d.a: expected %v got %v", i+1, tc.remote, got)
|
||||
}
|
||||
oi := fi.ToObjectInfo("bucket", "object")
|
||||
oi := fi.ToObjectInfo("bucket", "object", false)
|
||||
if got := oi.IsRemote(); got != tc.remote {
|
||||
t.Fatalf("Test %d.b: expected %v got %v", i+1, tc.remote, got)
|
||||
}
|
||||
|
||||
+18
-33
@@ -127,20 +127,12 @@ func (sys *BucketMetadataSys) Update(ctx context.Context, bucket string, configF
|
||||
meta.QuotaConfigJSON = configData
|
||||
meta.QuotaConfigUpdatedAt = UTCNow()
|
||||
case objectLockConfig:
|
||||
if !globalIsErasure && !globalIsDistErasure {
|
||||
return NotImplemented{}
|
||||
}
|
||||
meta.ObjectLockConfigXML = configData
|
||||
meta.ObjectLockConfigUpdatedAt = UTCNow()
|
||||
case bucketVersioningConfig:
|
||||
if !globalIsErasure && !globalIsDistErasure {
|
||||
return NotImplemented{}
|
||||
}
|
||||
meta.VersioningConfigXML = configData
|
||||
meta.VersioningConfigUpdatedAt = UTCNow()
|
||||
case bucketReplicationConfig:
|
||||
if !globalIsErasure && !globalIsDistErasure {
|
||||
return NotImplemented{}
|
||||
}
|
||||
meta.ReplicationConfigXML = configData
|
||||
meta.ReplicationConfigUpdatedAt = UTCNow()
|
||||
case bucketTargetsFile:
|
||||
@@ -193,12 +185,15 @@ func (sys *BucketMetadataSys) Get(bucket string) (BucketMetadata, error) {
|
||||
|
||||
// GetVersioningConfig returns configured versioning config
|
||||
// The returned object may not be modified.
|
||||
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, error) {
|
||||
func (sys *BucketMetadataSys) GetVersioningConfig(bucket string) (*versioning.Versioning, time.Time, error) {
|
||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, meta.Created, nil
|
||||
}
|
||||
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, time.Time{}, err
|
||||
}
|
||||
return meta.versioningConfig, nil
|
||||
return meta.versioningConfig, meta.VersioningConfigUpdatedAt, nil
|
||||
}
|
||||
|
||||
// GetTaggingConfig returns configured tagging config
|
||||
@@ -315,26 +310,27 @@ func (sys *BucketMetadataSys) CreatedAt(bucket string) (time.Time, error) {
|
||||
|
||||
// GetPolicyConfig returns configured bucket policy
|
||||
// The returned object may not be modified.
|
||||
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, error) {
|
||||
func (sys *BucketMetadataSys) GetPolicyConfig(bucket string) (*policy.Policy, time.Time, error) {
|
||||
if globalIsGateway {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return nil, errServerNotInitialized
|
||||
return nil, time.Time{}, errServerNotInitialized
|
||||
}
|
||||
return objAPI.GetBucketPolicy(GlobalContext, bucket)
|
||||
p, err := objAPI.GetBucketPolicy(GlobalContext, bucket)
|
||||
return p, UTCNow(), err
|
||||
}
|
||||
|
||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, err
|
||||
return nil, time.Time{}, err
|
||||
}
|
||||
if meta.policyConfig == nil {
|
||||
return nil, BucketPolicyNotFound{Bucket: bucket}
|
||||
return nil, time.Time{}, BucketPolicyNotFound{Bucket: bucket}
|
||||
}
|
||||
return meta.policyConfig, nil
|
||||
return meta.policyConfig, meta.PolicyConfigUpdatedAt, nil
|
||||
}
|
||||
|
||||
// GetQuotaConfig returns configured bucket quota
|
||||
@@ -369,6 +365,9 @@ func (sys *BucketMetadataSys) GetReplicationConfig(ctx context.Context, bucket s
|
||||
func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.BucketTargets, error) {
|
||||
meta, err := sys.GetConfig(GlobalContext, bucket)
|
||||
if err != nil {
|
||||
if errors.Is(err, errConfigNotFound) {
|
||||
return nil, BucketRemoteTargetNotFound{Bucket: bucket}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if meta.bucketTargetConfig == nil {
|
||||
@@ -377,20 +376,6 @@ func (sys *BucketMetadataSys) GetBucketTargetsConfig(bucket string) (*madmin.Buc
|
||||
return meta.bucketTargetConfig, nil
|
||||
}
|
||||
|
||||
// GetBucketTarget returns the target for the bucket and arn.
|
||||
func (sys *BucketMetadataSys) GetBucketTarget(bucket string, arn string) (madmin.BucketTarget, error) {
|
||||
targets, err := sys.GetBucketTargetsConfig(bucket)
|
||||
if err != nil {
|
||||
return madmin.BucketTarget{}, err
|
||||
}
|
||||
for _, t := range targets.Targets {
|
||||
if t.Arn == arn {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return madmin.BucketTarget{}, errConfigNotFound
|
||||
}
|
||||
|
||||
// GetConfig returns a specific configuration from the bucket metadata.
|
||||
// The returned object may not be modified.
|
||||
func (sys *BucketMetadataSys) GetConfig(ctx context.Context, bucket string) (BucketMetadata, error) {
|
||||
|
||||
@@ -87,6 +87,7 @@ type BucketMetadata struct {
|
||||
TaggingConfigUpdatedAt time.Time
|
||||
QuotaConfigUpdatedAt time.Time
|
||||
ReplicationConfigUpdatedAt time.Time
|
||||
VersioningConfigUpdatedAt time.Time
|
||||
|
||||
// Unexported fields. Must be updated atomically.
|
||||
policyConfig *policy.Policy
|
||||
@@ -364,6 +365,7 @@ func (b *BucketMetadata) defaultTimestamps() {
|
||||
if b.PolicyConfigUpdatedAt.IsZero() {
|
||||
b.PolicyConfigUpdatedAt = b.Created
|
||||
}
|
||||
|
||||
if b.EncryptionConfigUpdatedAt.IsZero() {
|
||||
b.EncryptionConfigUpdatedAt = b.Created
|
||||
}
|
||||
@@ -383,6 +385,10 @@ func (b *BucketMetadata) defaultTimestamps() {
|
||||
if b.ReplicationConfigUpdatedAt.IsZero() {
|
||||
b.ReplicationConfigUpdatedAt = b.Created
|
||||
}
|
||||
|
||||
if b.VersioningConfigUpdatedAt.IsZero() {
|
||||
b.VersioningConfigUpdatedAt = b.Created
|
||||
}
|
||||
}
|
||||
|
||||
// Save config to supplied ObjectLayer api.
|
||||
|
||||
@@ -144,6 +144,12 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
case "VersioningConfigUpdatedAt":
|
||||
z.VersioningConfigUpdatedAt, err = dc.ReadTime()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
@@ -157,9 +163,9 @@ func (z *BucketMetadata) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
// map header, size 20
|
||||
// map header, size 21
|
||||
// write "Name"
|
||||
err = en.Append(0xde, 0x0, 0x14, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
err = en.Append(0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -358,15 +364,25 @@ func (z *BucketMetadata) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
// write "VersioningConfigUpdatedAt"
|
||||
err = en.Append(0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = en.WriteTime(z.VersioningConfigUpdatedAt)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
// map header, size 20
|
||||
// map header, size 21
|
||||
// string "Name"
|
||||
o = append(o, 0xde, 0x0, 0x14, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
o = append(o, 0xde, 0x0, 0x15, 0xa4, 0x4e, 0x61, 0x6d, 0x65)
|
||||
o = msgp.AppendString(o, z.Name)
|
||||
// string "Created"
|
||||
o = append(o, 0xa7, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64)
|
||||
@@ -425,6 +441,9 @@ func (z *BucketMetadata) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
// string "ReplicationConfigUpdatedAt"
|
||||
o = append(o, 0xba, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||
o = msgp.AppendTime(o, z.ReplicationConfigUpdatedAt)
|
||||
// string "VersioningConfigUpdatedAt"
|
||||
o = append(o, 0xb9, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74)
|
||||
o = msgp.AppendTime(o, z.VersioningConfigUpdatedAt)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -566,6 +585,12 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
err = msgp.WrapError(err, "ReplicationConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
case "VersioningConfigUpdatedAt":
|
||||
z.VersioningConfigUpdatedAt, bts, err = msgp.ReadTimeBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, "VersioningConfigUpdatedAt")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
@@ -580,6 +605,6 @@ func (z *BucketMetadata) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z *BucketMetadata) Msgsize() (s int) {
|
||||
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize
|
||||
s = 3 + 5 + msgp.StringPrefixSize + len(z.Name) + 8 + msgp.TimeSize + 12 + msgp.BoolSize + 17 + msgp.BytesPrefixSize + len(z.PolicyConfigJSON) + 22 + msgp.BytesPrefixSize + len(z.NotificationConfigXML) + 19 + msgp.BytesPrefixSize + len(z.LifecycleConfigXML) + 20 + msgp.BytesPrefixSize + len(z.ObjectLockConfigXML) + 20 + msgp.BytesPrefixSize + len(z.VersioningConfigXML) + 20 + msgp.BytesPrefixSize + len(z.EncryptionConfigXML) + 17 + msgp.BytesPrefixSize + len(z.TaggingConfigXML) + 16 + msgp.BytesPrefixSize + len(z.QuotaConfigJSON) + 21 + msgp.BytesPrefixSize + len(z.ReplicationConfigXML) + 24 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigJSON) + 28 + msgp.BytesPrefixSize + len(z.BucketTargetsConfigMetaJSON) + 22 + msgp.TimeSize + 26 + msgp.TimeSize + 26 + msgp.TimeSize + 23 + msgp.TimeSize + 21 + msgp.TimeSize + 27 + msgp.TimeSize + 26 + msgp.TimeSize
|
||||
return
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ type PolicySys struct{}
|
||||
|
||||
// Get returns stored bucket policy
|
||||
func (sys *PolicySys) Get(bucket string) (*policy.Policy, error) {
|
||||
return globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||
policy, _, err := globalBucketMetadataSys.GetPolicyConfig(bucket)
|
||||
return policy, err
|
||||
}
|
||||
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
)
|
||||
|
||||
// PutBucketReplicationConfigHandler - PUT Bucket replication configuration.
|
||||
// ----------
|
||||
// Add a replication configuration on the specified bucket as specified in https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketReplication.html
|
||||
func (api objectAPIHandlers) PutBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketReplicationConfig")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
if globalIsGateway {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if versioned := globalBucketVersioningSys.Enabled(bucket); !versioned {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNeedsVersioningError), r.URL)
|
||||
return
|
||||
}
|
||||
replicationConfig, err := replication.ParseConfig(io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
apiErr := errorCodes.ToAPIErr(ErrMalformedXML)
|
||||
apiErr.Description = err.Error()
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
sameTarget, apiErr := validateReplicationDestination(ctx, bucket, replicationConfig, true)
|
||||
if apiErr != noError {
|
||||
writeErrorResponse(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
// Validate the received bucket replication config
|
||||
if err = replicationConfig.Validate(bucket, sameTarget); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(replicationConfig)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, configData); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketReplicationConfigHandler - GET Bucket replication configuration.
|
||||
// ----------
|
||||
// Gets the replication configuration for a bucket.
|
||||
func (api objectAPIHandlers) GetBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketReplicationConfig")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
configData, err := xml.Marshal(config)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseXML(w, configData)
|
||||
}
|
||||
|
||||
// DeleteBucketReplicationConfigHandler - DELETE Bucket replication config.
|
||||
// ----------
|
||||
func (api objectAPIHandlers) DeleteBucketReplicationConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "DeleteBucketReplicationConfig")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if globalSiteReplicationSys.isEnabled() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationDenyEditError), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalBucketMetadataSys.Update(ctx, bucket, bucketReplicationConfig, nil); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetBucketReplicationMetricsHandler - GET Bucket replication metrics.
|
||||
// ----------
|
||||
// Gets the replication metrics for a bucket.
|
||||
func (api objectAPIHandlers) GetBucketReplicationMetricsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketReplicationMetrics")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// check if user has permissions to perform this operation
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetReplicationConfigurationAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
var usageInfo BucketUsageInfo
|
||||
dataUsageInfo, err := loadDataUsageFromBackend(ctx, objectAPI)
|
||||
if err == nil && !dataUsageInfo.LastUpdate.IsZero() {
|
||||
usageInfo = dataUsageInfo.BucketsUsage[bucket]
|
||||
}
|
||||
|
||||
w.Header().Set(xhttp.ContentType, string(mimeJSON))
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err = enc.Encode(getLatestReplicationStats(bucket, usageInfo)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ResetBucketReplicationStartHandler - starts a replication reset for all objects in a bucket which
|
||||
// qualify for replication and re-sync the object(s) to target, provided ExistingObjectReplication is
|
||||
// enabled for the qualifying rule. This API is a MinIO only extension provided for situations where
|
||||
// remote target is entirely lost,and previously replicated objects need to be re-synced. If resync is
|
||||
// already in progress it returns an error
|
||||
func (api objectAPIHandlers) ResetBucketReplicationStartHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ResetBucketReplicationStart")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
durationStr := r.URL.Query().Get("older-than")
|
||||
arn := r.URL.Query().Get("arn")
|
||||
resetID := r.URL.Query().Get("reset-id")
|
||||
if resetID == "" {
|
||||
resetID = mustGetUUID()
|
||||
}
|
||||
var (
|
||||
days time.Duration
|
||||
err error
|
||||
)
|
||||
if durationStr != "" {
|
||||
days, err = time.ParseDuration(durationStr)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("invalid query parameter older-than %s for %s : %w", durationStr, bucket, err),
|
||||
}), r.URL)
|
||||
}
|
||||
}
|
||||
resetBeforeDate := UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
config, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if !config.HasExistingObjectReplication(arn) {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrReplicationNoExistingObjects), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
tgtArns := config.FilterTargetArns(
|
||||
replication.ObjectOpts{
|
||||
OpType: replication.ResyncReplicationType,
|
||||
TargetArn: arn,
|
||||
})
|
||||
|
||||
if len(tgtArns) == 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("Remote target ARN %s missing or ineligible for replication resync", arn),
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if len(tgtArns) > 1 && arn == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("ARN should be specified for replication reset"),
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
var rinfo ResyncTargetsInfo
|
||||
target := globalBucketTargetSys.GetRemoteBucketTargetByArn(ctx, bucket, tgtArns[0])
|
||||
target.ResetBeforeDate = UTCNow().AddDate(0, 0, -1*int(days/24))
|
||||
target.ResetID = resetID
|
||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{Arn: tgtArns[0], ResetID: target.ResetID})
|
||||
if err = globalBucketTargetSys.SetTarget(ctx, bucket, &target, true); err != nil {
|
||||
switch err.(type) {
|
||||
case BucketRemoteConnectionErr:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrReplicationRemoteConnectionError, err), r.URL)
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
}
|
||||
}
|
||||
if err := startReplicationResync(ctx, bucket, arn, resetID, resetBeforeDate, objectAPI); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: err,
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(rinfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, data)
|
||||
}
|
||||
|
||||
// ResetBucketReplicationStatusHandler - returns the status of replication reset.
|
||||
// This API is a MinIO only extension
|
||||
func (api objectAPIHandlers) ResetBucketReplicationStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "ResetBucketReplicationStatus")
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
arn := r.URL.Query().Get("arn")
|
||||
var err error
|
||||
|
||||
objectAPI := api.ObjectAPI()
|
||||
if objectAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.ResetBucketReplicationStateAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists.
|
||||
if _, err := objectAPI.GetBucketInfo(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, _, err := globalBucketMetadataSys.GetReplicationConfig(ctx, bucket); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
globalReplicationPool.resyncState.RLock()
|
||||
brs, ok := globalReplicationPool.resyncState.statusMap[bucket]
|
||||
globalReplicationPool.resyncState.RUnlock()
|
||||
if !ok {
|
||||
brs, err = loadBucketResyncMetadata(ctx, bucket, objectAPI)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErrWithErr(ErrBadRequest, InvalidArgument{
|
||||
Bucket: bucket,
|
||||
Err: fmt.Errorf("No replication resync status available for %s", arn),
|
||||
}), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var rinfo ResyncTargetsInfo
|
||||
for tarn, st := range brs.TargetsMap {
|
||||
if arn != "" && tarn != arn {
|
||||
continue
|
||||
}
|
||||
rinfo.Targets = append(rinfo.Targets, ResyncTarget{
|
||||
Arn: tarn,
|
||||
ResetID: st.ResyncID,
|
||||
StartTime: st.StartTime,
|
||||
EndTime: st.EndTime,
|
||||
ResyncStatus: st.ResyncStatus.String(),
|
||||
ReplicatedSize: st.ReplicatedSize,
|
||||
ReplicatedCount: st.ReplicatedCount,
|
||||
FailedSize: st.FailedSize,
|
||||
FailedCount: st.FailedCount,
|
||||
Bucket: st.Bucket,
|
||||
Object: st.Object,
|
||||
})
|
||||
}
|
||||
data, err := json.Marshal(rinfo)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write success response.
|
||||
writeSuccessResponseJSON(w, data)
|
||||
}
|
||||
@@ -135,6 +135,23 @@ func (r *ReplicationStats) GetInitialUsage(bucket string) BucketReplicationStats
|
||||
return st.Clone()
|
||||
}
|
||||
|
||||
// GetAll returns replication metrics for all buckets at once.
|
||||
func (r *ReplicationStats) GetAll() map[string]BucketReplicationStats {
|
||||
if r == nil {
|
||||
return map[string]BucketReplicationStats{}
|
||||
}
|
||||
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
|
||||
bucketReplicationStats := make(map[string]BucketReplicationStats, len(r.Cache))
|
||||
for k, v := range r.Cache {
|
||||
bucketReplicationStats[k] = v.Clone()
|
||||
}
|
||||
|
||||
return bucketReplicationStats
|
||||
}
|
||||
|
||||
// Get replication metrics for a bucket from this node since this node came up.
|
||||
func (r *ReplicationStats) Get(bucket string) BucketReplicationStats {
|
||||
if r == nil {
|
||||
@@ -161,7 +178,7 @@ func NewReplicationStats(ctx context.Context, objectAPI ObjectLayer) *Replicatio
|
||||
|
||||
// load replication metrics at cluster start from initial data usage
|
||||
func (r *ReplicationStats) loadInitialReplicationMetrics(ctx context.Context) {
|
||||
rTimer := time.NewTimer(time.Minute * 1)
|
||||
rTimer := time.NewTimer(time.Minute)
|
||||
defer rTimer.Stop()
|
||||
var (
|
||||
dui DataUsageInfo
|
||||
@@ -174,13 +191,12 @@ outer:
|
||||
return
|
||||
case <-rTimer.C:
|
||||
dui, err = loadDataUsageFromBackend(GlobalContext, newObjectLayerFn())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// If LastUpdate is set, data usage is available.
|
||||
if !dui.LastUpdate.IsZero() {
|
||||
if err == nil && !dui.LastUpdate.IsZero() {
|
||||
break outer
|
||||
}
|
||||
|
||||
rTimer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -509,7 +509,7 @@ func getHealReplicateObjectInfo(objInfo ObjectInfo, rcfg replicationConfig) Repl
|
||||
ObjectName: oi.Name,
|
||||
VersionID: oi.VersionID,
|
||||
},
|
||||
}, oi, ObjectOptions{}, nil)
|
||||
}, oi, ObjectOptions{VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name)}, nil)
|
||||
} else {
|
||||
dsc = mustReplicate(GlobalContext, oi.Bucket, oi.Name, getMustReplicateOptions(ObjectInfo{
|
||||
UserDefined: oi.UserDefined,
|
||||
|
||||
+84
-32
@@ -87,7 +87,7 @@ func getReplicationConfig(ctx context.Context, bucketName string) (rc *replicati
|
||||
|
||||
// validateReplicationDestination returns error if replication destination bucket missing or not configured
|
||||
// It also returns true if replication destination is same as this server.
|
||||
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config) (bool, APIError) {
|
||||
func validateReplicationDestination(ctx context.Context, bucket string, rCfg *replication.Config, checkRemote bool) (bool, APIError) {
|
||||
var arns []string
|
||||
if rCfg.RoleArn != "" {
|
||||
arns = append(arns, rCfg.RoleArn)
|
||||
@@ -96,26 +96,29 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
||||
arns = append(arns, rule.Destination.String())
|
||||
}
|
||||
}
|
||||
var sameTarget bool
|
||||
for _, arnStr := range arns {
|
||||
arn, err := madmin.ParseARN(arnStr)
|
||||
if err != nil {
|
||||
return false, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
|
||||
return sameTarget, errorCodes.ToAPIErrWithErr(ErrBucketRemoteArnInvalid, err)
|
||||
}
|
||||
if arn.Type != madmin.ReplicationService {
|
||||
return false, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
|
||||
return sameTarget, toAPIError(ctx, BucketRemoteArnTypeInvalid{Bucket: bucket})
|
||||
}
|
||||
clnt := globalBucketTargetSys.GetRemoteTargetClient(ctx, arnStr)
|
||||
if clnt == nil {
|
||||
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||
return sameTarget, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||
}
|
||||
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
|
||||
return false, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
||||
}
|
||||
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
|
||||
if ret.LockEnabled {
|
||||
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
|
||||
if err != nil || lock != "Enabled" {
|
||||
return false, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
|
||||
if checkRemote { // validate remote bucket
|
||||
if found, err := clnt.BucketExists(ctx, arn.Bucket); !found {
|
||||
return sameTarget, errorCodes.ToAPIErrWithErr(ErrRemoteDestinationNotFoundError, err)
|
||||
}
|
||||
if ret, err := globalBucketObjectLockSys.Get(bucket); err == nil {
|
||||
if ret.LockEnabled {
|
||||
lock, _, _, _, err := clnt.GetObjectLockConfig(ctx, arn.Bucket)
|
||||
if err != nil || lock != "Enabled" {
|
||||
return sameTarget, errorCodes.ToAPIErrWithErr(ErrReplicationDestinationMissingLock, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,12 +126,19 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
||||
c, ok := globalBucketTargetSys.arnRemotesMap[arnStr]
|
||||
if ok {
|
||||
if c.EndpointURL().String() == clnt.EndpointURL().String() {
|
||||
sameTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
||||
return sameTarget, toAPIError(ctx, nil)
|
||||
selfTarget, _ := isLocalHost(clnt.EndpointURL().Hostname(), clnt.EndpointURL().Port(), globalMinioPort)
|
||||
if !sameTarget {
|
||||
sameTarget = selfTarget
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||
|
||||
if len(arns) == 0 {
|
||||
return false, toAPIError(ctx, BucketRemoteTargetNotFound{Bucket: bucket})
|
||||
}
|
||||
return sameTarget, toAPIError(ctx, nil)
|
||||
}
|
||||
|
||||
type mustReplicateOptions struct {
|
||||
@@ -180,6 +190,17 @@ func mustReplicate(ctx context.Context, bucket, object string, mopts mustReplica
|
||||
return
|
||||
}
|
||||
|
||||
// object layer not initialized we return with no decision.
|
||||
if newObjectLayerFn() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Disable server-side replication on object prefixes which are excluded
|
||||
// from versioning via the MinIO bucket versioning extension.
|
||||
if globalBucketVersioningSys.PrefixSuspended(bucket, object) {
|
||||
return
|
||||
}
|
||||
|
||||
replStatus := mopts.ReplicationStatus()
|
||||
if replStatus == replication.Replica && !mopts.isMetadataReplication() {
|
||||
return
|
||||
@@ -264,6 +285,11 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
|
||||
if delOpts.ReplicationRequest {
|
||||
return
|
||||
}
|
||||
// Skip replication if this object's prefix is excluded from being
|
||||
// versioned.
|
||||
if !delOpts.Versioned {
|
||||
return
|
||||
}
|
||||
opts := replication.ObjectOpts{
|
||||
Name: dobj.ObjectName,
|
||||
SSEC: crypto.SSEC.IsEncrypted(oi.UserDefined),
|
||||
@@ -457,8 +483,8 @@ func replicateDelete(ctx context.Context, dobj DeletedObjectReplicationInfo, obj
|
||||
VersionID: versionID,
|
||||
MTime: dobj.DeleteMarkerMTime.Time,
|
||||
DeleteReplication: drs,
|
||||
Versioned: globalBucketVersioningSys.Enabled(bucket),
|
||||
VersionSuspended: globalBucketVersioningSys.Suspended(bucket),
|
||||
Versioned: globalBucketVersioningSys.PrefixEnabled(bucket, dobj.ObjectName),
|
||||
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(bucket, dobj.ObjectName),
|
||||
})
|
||||
if err != nil && !isErrVersionNotFound(err) { // VersionNotFound would be reported by pool that object version is missing on.
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to update replication metadata for %s/%s(%s): %s", bucket, dobj.ObjectName, versionID, err))
|
||||
@@ -1019,8 +1045,13 @@ func replicateObjectToTarget(ctx context.Context, ri ReplicateObjectInfo, object
|
||||
return
|
||||
}
|
||||
|
||||
versioned := globalBucketVersioningSys.PrefixEnabled(bucket, object)
|
||||
versionSuspended := globalBucketVersioningSys.PrefixSuspended(bucket, object)
|
||||
|
||||
gr, err = objectAPI.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, readLock, ObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
VersionID: objInfo.VersionID,
|
||||
Versioned: versioned,
|
||||
VersionSuspended: versionSuspended,
|
||||
})
|
||||
if err != nil {
|
||||
sendEvent(eventArgs{
|
||||
@@ -1590,7 +1621,11 @@ func proxyGetToReplicationTarget(ctx context.Context, bucket, object string, rs
|
||||
return reader, proxyResult{Proxy: true}, nil
|
||||
}
|
||||
|
||||
func getproxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
|
||||
func getProxyTargets(ctx context.Context, bucket, object string, opts ObjectOptions) (tgts *madmin.BucketTargets) {
|
||||
if opts.VersionSuspended {
|
||||
return &madmin.BucketTargets{}
|
||||
}
|
||||
|
||||
cfg, err := getReplicationConfig(ctx, bucket)
|
||||
if err != nil || cfg == nil {
|
||||
return &madmin.BucketTargets{}
|
||||
@@ -1727,10 +1762,6 @@ func (c replicationConfig) Resync(ctx context.Context, oi ObjectInfo, dsc *Repli
|
||||
if c.Empty() {
|
||||
return
|
||||
}
|
||||
// existing object replication does not apply to un-versioned objects
|
||||
if oi.VersionID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Now overlay existing object replication choices for target
|
||||
if oi.DeleteMarker {
|
||||
@@ -1831,9 +1862,26 @@ func resyncTarget(oi ObjectInfo, arn string, resetID string, resetBeforeDate tim
|
||||
return
|
||||
}
|
||||
|
||||
// get the most current of in-memory replication stats and data usage info from crawler.
|
||||
func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplicationStats) {
|
||||
bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
|
||||
func getAllLatestReplicationStats(bucketsUsage map[string]BucketUsageInfo) (bucketsReplicationStats map[string]BucketReplicationStats) {
|
||||
peerBucketStatsList := globalNotificationSys.GetClusterAllBucketStats(GlobalContext)
|
||||
bucketsReplicationStats = make(map[string]BucketReplicationStats, len(bucketsUsage))
|
||||
|
||||
for bucket, u := range bucketsUsage {
|
||||
bucketStats := make([]BucketStats, len(peerBucketStatsList))
|
||||
for i, peerBucketStats := range peerBucketStatsList {
|
||||
bucketStat, ok := peerBucketStats[bucket]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
bucketStats[i] = bucketStat
|
||||
}
|
||||
bucketsReplicationStats[bucket] = calculateBucketReplicationStats(bucket, u, bucketStats)
|
||||
}
|
||||
|
||||
return bucketsReplicationStats
|
||||
}
|
||||
|
||||
func calculateBucketReplicationStats(bucket string, u BucketUsageInfo, bucketStats []BucketStats) (s BucketReplicationStats) {
|
||||
// accumulate cluster bucket stats
|
||||
stats := make(map[string]*BucketReplicationStat)
|
||||
var totReplicaSize int64
|
||||
@@ -1902,6 +1950,12 @@ func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplic
|
||||
return s
|
||||
}
|
||||
|
||||
// get the most current of in-memory replication stats and data usage info from crawler.
|
||||
func getLatestReplicationStats(bucket string, u BucketUsageInfo) (s BucketReplicationStats) {
|
||||
bucketStats := globalNotificationSys.GetClusterBucketStats(GlobalContext, bucket)
|
||||
return calculateBucketReplicationStats(bucket, u, bucketStats)
|
||||
}
|
||||
|
||||
const resyncTimeInterval = time.Minute * 10
|
||||
|
||||
// periodicResyncMetaSave saves in-memory resync meta stats to disk in periodic intervals
|
||||
@@ -1912,7 +1966,6 @@ func (p *ReplicationPool) periodicResyncMetaSave(ctx context.Context, objectAPI
|
||||
for {
|
||||
select {
|
||||
case <-resyncTimer.C:
|
||||
resyncTimer.Reset(resyncTimeInterval)
|
||||
now := UTCNow()
|
||||
p.resyncState.RLock()
|
||||
for bucket, brs := range p.resyncState.statusMap {
|
||||
@@ -1933,6 +1986,8 @@ func (p *ReplicationPool) periodicResyncMetaSave(ctx context.Context, objectAPI
|
||||
}
|
||||
}
|
||||
p.resyncState.RUnlock()
|
||||
|
||||
resyncTimer.Reset(resyncTimeInterval)
|
||||
case <-ctx.Done():
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
@@ -1952,6 +2007,7 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
||||
st.EndTime = UTCNow()
|
||||
st.ResyncStatus = resyncStatus
|
||||
m.TargetsMap[arn] = st
|
||||
globalReplicationPool.resyncState.statusMap[bucket] = m
|
||||
globalReplicationPool.resyncState.Unlock()
|
||||
}()
|
||||
// Allocate new results channel to receive ObjectInfo.
|
||||
@@ -2012,7 +2068,6 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
||||
}
|
||||
|
||||
if roi.DeleteMarker || !roi.VersionPurgeStatus.Empty() {
|
||||
|
||||
versionID := ""
|
||||
dmVersionID := ""
|
||||
if roi.VersionPurgeStatus.Empty() {
|
||||
@@ -2059,6 +2114,7 @@ func resyncBucket(ctx context.Context, bucket, arn string, heal bool, objectAPI
|
||||
st.ReplicatedSize += roi.Size
|
||||
}
|
||||
m.TargetsMap[arn] = st
|
||||
globalReplicationPool.resyncState.statusMap[bucket] = m
|
||||
globalReplicationPool.resyncState.Unlock()
|
||||
}
|
||||
resyncStatus = ResyncCompleted
|
||||
@@ -2139,10 +2195,6 @@ func (p *ReplicationPool) initResync(ctx context.Context, buckets []BucketInfo,
|
||||
if objAPI == nil {
|
||||
return errServerNotInitialized
|
||||
}
|
||||
// replication applies only to erasure coded setups
|
||||
if !globalIsErasure {
|
||||
return nil
|
||||
}
|
||||
// Load bucket metadata sys in background
|
||||
go p.loadResync(ctx, buckets, objAPI)
|
||||
return nil
|
||||
|
||||
@@ -51,6 +51,9 @@ func (rl *ReplicationLatency) update(size int64, duration time.Duration) {
|
||||
rl.UploadHistogram.Add(size, duration)
|
||||
}
|
||||
|
||||
// BucketStatsMap captures bucket statistics for all buckets
|
||||
type BucketStatsMap map[string]BucketStats
|
||||
|
||||
// BucketStats bucket statistics
|
||||
type BucketStats struct {
|
||||
ReplicationStats BucketReplicationStats
|
||||
|
||||
@@ -792,6 +792,183 @@ func (z *BucketStats) Msgsize() (s int) {
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *BucketStatsMap) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var zb0003 uint32
|
||||
zb0003, err = dc.ReadMapHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if (*z) == nil {
|
||||
(*z) = make(BucketStatsMap, zb0003)
|
||||
} else if len((*z)) > 0 {
|
||||
for key := range *z {
|
||||
delete((*z), key)
|
||||
}
|
||||
}
|
||||
for zb0003 > 0 {
|
||||
zb0003--
|
||||
var zb0001 string
|
||||
var zb0002 BucketStats
|
||||
zb0001, err = dc.ReadString()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
var field []byte
|
||||
_ = field
|
||||
var zb0004 uint32
|
||||
zb0004, err = dc.ReadMapHeader()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
for zb0004 > 0 {
|
||||
zb0004--
|
||||
field, err = dc.ReadMapKeyPtr()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
switch msgp.UnsafeString(field) {
|
||||
case "ReplicationStats":
|
||||
err = zb0002.ReplicationStats.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001, "ReplicationStats")
|
||||
return
|
||||
}
|
||||
default:
|
||||
err = dc.Skip()
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
(*z)[zb0001] = zb0002
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// EncodeMsg implements msgp.Encodable
|
||||
func (z BucketStatsMap) EncodeMsg(en *msgp.Writer) (err error) {
|
||||
err = en.WriteMapHeader(uint32(len(z)))
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
for zb0005, zb0006 := range z {
|
||||
err = en.WriteString(zb0005)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
// map header, size 1
|
||||
// write "ReplicationStats"
|
||||
err = en.Append(0x81, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = zb0006.ReplicationStats.EncodeMsg(en)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0005, "ReplicationStats")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MarshalMsg implements msgp.Marshaler
|
||||
func (z BucketStatsMap) MarshalMsg(b []byte) (o []byte, err error) {
|
||||
o = msgp.Require(b, z.Msgsize())
|
||||
o = msgp.AppendMapHeader(o, uint32(len(z)))
|
||||
for zb0005, zb0006 := range z {
|
||||
o = msgp.AppendString(o, zb0005)
|
||||
// map header, size 1
|
||||
// string "ReplicationStats"
|
||||
o = append(o, 0x81, 0xb0, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73)
|
||||
o, err = zb0006.ReplicationStats.MarshalMsg(o)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0005, "ReplicationStats")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UnmarshalMsg implements msgp.Unmarshaler
|
||||
func (z *BucketStatsMap) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
||||
var zb0003 uint32
|
||||
zb0003, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
if (*z) == nil {
|
||||
(*z) = make(BucketStatsMap, zb0003)
|
||||
} else if len((*z)) > 0 {
|
||||
for key := range *z {
|
||||
delete((*z), key)
|
||||
}
|
||||
}
|
||||
for zb0003 > 0 {
|
||||
var zb0001 string
|
||||
var zb0002 BucketStats
|
||||
zb0003--
|
||||
zb0001, bts, err = msgp.ReadStringBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err)
|
||||
return
|
||||
}
|
||||
var field []byte
|
||||
_ = field
|
||||
var zb0004 uint32
|
||||
zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
for zb0004 > 0 {
|
||||
zb0004--
|
||||
field, bts, err = msgp.ReadMapKeyZC(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
switch msgp.UnsafeString(field) {
|
||||
case "ReplicationStats":
|
||||
bts, err = zb0002.ReplicationStats.UnmarshalMsg(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001, "ReplicationStats")
|
||||
return
|
||||
}
|
||||
default:
|
||||
bts, err = msgp.Skip(bts)
|
||||
if err != nil {
|
||||
err = msgp.WrapError(err, zb0001)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
(*z)[zb0001] = zb0002
|
||||
}
|
||||
o = bts
|
||||
return
|
||||
}
|
||||
|
||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
||||
func (z BucketStatsMap) Msgsize() (s int) {
|
||||
s = msgp.MapHeaderSize
|
||||
if z != nil {
|
||||
for zb0005, zb0006 := range z {
|
||||
_ = zb0006
|
||||
s += msgp.StringPrefixSize + len(zb0005) + 1 + 17 + zb0006.ReplicationStats.Msgsize()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DecodeMsg implements msgp.Decodable
|
||||
func (z *ReplicationLatency) DecodeMsg(dc *msgp.Reader) (err error) {
|
||||
var field []byte
|
||||
|
||||
@@ -348,6 +348,119 @@ func BenchmarkDecodeBucketStats(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalBucketStatsMap(t *testing.T) {
|
||||
v := BucketStatsMap{}
|
||||
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 BenchmarkMarshalMsgBucketStatsMap(b *testing.B) {
|
||||
v := BucketStatsMap{}
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.MarshalMsg(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendMsgBucketStatsMap(b *testing.B) {
|
||||
v := BucketStatsMap{}
|
||||
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 BenchmarkUnmarshalBucketStatsMap(b *testing.B) {
|
||||
v := BucketStatsMap{}
|
||||
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 TestEncodeDecodeBucketStatsMap(t *testing.T) {
|
||||
v := BucketStatsMap{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
|
||||
m := v.Msgsize()
|
||||
if buf.Len() > m {
|
||||
t.Log("WARNING: TestEncodeDecodeBucketStatsMap Msgsize() is inaccurate")
|
||||
}
|
||||
|
||||
vn := BucketStatsMap{}
|
||||
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 BenchmarkEncodeBucketStatsMap(b *testing.B) {
|
||||
v := BucketStatsMap{}
|
||||
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 BenchmarkDecodeBucketStatsMap(b *testing.B) {
|
||||
v := BucketStatsMap{}
|
||||
var buf bytes.Buffer
|
||||
msgp.Encode(&buf, &v)
|
||||
b.SetBytes(int64(buf.Len()))
|
||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
||||
dc := msgp.NewReader(rd)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := v.DecodeMsg(dc)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalReplicationLatency(t *testing.T) {
|
||||
v := ReplicationLatency{}
|
||||
bts, err := v.MarshalMsg(nil)
|
||||
|
||||
@@ -120,9 +120,6 @@ func (sys *BucketTargetSys) SetTarget(ctx context.Context, bucket string, tgt *m
|
||||
return BucketRemoteConnectionErr{Bucket: tgt.TargetBucket, Err: err}
|
||||
}
|
||||
if tgt.Type == madmin.ReplicationService {
|
||||
if !globalIsErasure {
|
||||
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
|
||||
}
|
||||
if !globalBucketVersioningSys.Enabled(bucket) {
|
||||
return BucketReplicationSourceNotVersioned{Bucket: bucket}
|
||||
}
|
||||
@@ -184,9 +181,6 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
|
||||
if globalIsGateway {
|
||||
return nil
|
||||
}
|
||||
if !globalIsErasure {
|
||||
return NotImplemented{Message: "Replication is not implemented in " + getMinioMode()}
|
||||
}
|
||||
|
||||
if arnStr == "" {
|
||||
return BucketRemoteArnInvalid{Bucket: bucket}
|
||||
@@ -201,7 +195,7 @@ func (sys *BucketTargetSys) RemoveTarget(ctx context.Context, bucket, arnStr str
|
||||
// reject removal of remote target if replication configuration is present
|
||||
rcfg, err := getReplicationConfig(ctx, bucket)
|
||||
if err == nil {
|
||||
for _, tgtArn := range rcfg.FilterTargetArns(replication.ObjectOpts{}) {
|
||||
for _, tgtArn := range rcfg.FilterTargetArns(replication.ObjectOpts{OpType: replication.AllReplicationType}) {
|
||||
if err == nil && (tgtArn == arnStr || rcfg.RoleArn == arnStr) {
|
||||
sys.RLock()
|
||||
_, ok := sys.arnRemotesMap[arnStr]
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/bucket/policy"
|
||||
@@ -63,28 +65,28 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if globalSiteReplicationSys.isEnabled() {
|
||||
if globalSiteReplicationSys.isEnabled() && !v.Enabled() {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "Cluster replication is enabled for this site, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
Description: "Cluster replication is enabled on this site, versioning cannot be suspended on bucket.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && v.Suspended() {
|
||||
if rcfg, _ := globalBucketObjectLockSys.Get(bucket); rcfg.LockEnabled && (v.Suspended() || v.PrefixesExcluded()) {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "An Object Lock configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
Description: "An Object Lock configuration is present on this bucket, versioning cannot be suspended.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
if _, err := getReplicationConfig(ctx, bucket); err == nil && v.Suspended() {
|
||||
writeErrorResponse(ctx, w, APIError{
|
||||
Code: "InvalidBucketState",
|
||||
Description: "A replication configuration is present on this bucket, so the versioning state cannot be changed.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
Description: "A replication configuration is present on this bucket, bucket wide versioning cannot be suspended.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}, r.URL)
|
||||
return
|
||||
}
|
||||
@@ -100,6 +102,20 @@ func (api objectAPIHandlers) PutBucketVersioningHandler(w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
// Call site replication hook.
|
||||
//
|
||||
// We encode the xml bytes as base64 to ensure there are no encoding
|
||||
// errors.
|
||||
cfgStr := base64.StdEncoding.EncodeToString(configData)
|
||||
if err = globalSiteReplicationSys.BucketMetaHook(ctx, madmin.SRBucketMeta{
|
||||
Type: madmin.SRBucketMetaTypeVersionConfig,
|
||||
Bucket: bucket,
|
||||
Versioning: &cfgStr,
|
||||
}); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
|
||||
+40
-15
@@ -17,44 +17,69 @@
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/minio/minio/internal/bucket/versioning"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/versioning"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
// BucketVersioningSys - policy subsystem.
|
||||
type BucketVersioningSys struct{}
|
||||
|
||||
// Enabled enabled versioning?
|
||||
func (sys *BucketVersioningSys) Enabled(bucket string) bool {
|
||||
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||
vc, err := sys.Get(bucket)
|
||||
if err != nil {
|
||||
return false
|
||||
logger.CriticalIf(GlobalContext, err)
|
||||
}
|
||||
return vc.Enabled()
|
||||
}
|
||||
|
||||
// PrefixEnabled returns true is versioning is enabled at bucket level and if
|
||||
// the given prefix doesn't match any excluded prefixes pattern. This is
|
||||
// part of a MinIO versioning configuration extension.
|
||||
func (sys *BucketVersioningSys) PrefixEnabled(bucket, prefix string) bool {
|
||||
vc, err := sys.Get(bucket)
|
||||
if err != nil {
|
||||
logger.CriticalIf(GlobalContext, err)
|
||||
}
|
||||
return vc.PrefixEnabled(prefix)
|
||||
}
|
||||
|
||||
// Suspended suspended versioning?
|
||||
func (sys *BucketVersioningSys) Suspended(bucket string) bool {
|
||||
vc, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||
vc, err := sys.Get(bucket)
|
||||
if err != nil {
|
||||
return false
|
||||
logger.CriticalIf(GlobalContext, err)
|
||||
}
|
||||
return vc.Suspended()
|
||||
}
|
||||
|
||||
// PrefixSuspended returns true if the given prefix matches an excluded prefix
|
||||
// pattern. This is part of a MinIO versioning configuration extension.
|
||||
func (sys *BucketVersioningSys) PrefixSuspended(bucket, prefix string) bool {
|
||||
vc, err := sys.Get(bucket)
|
||||
if err != nil {
|
||||
logger.CriticalIf(GlobalContext, err)
|
||||
}
|
||||
|
||||
return vc.PrefixSuspended(prefix)
|
||||
}
|
||||
|
||||
// Get returns stored bucket policy
|
||||
func (sys *BucketVersioningSys) Get(bucket string) (*versioning.Versioning, error) {
|
||||
if globalIsGateway {
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return nil, errServerNotInitialized
|
||||
}
|
||||
return nil, NotImplemented{}
|
||||
// Gateway does not implement versioning.
|
||||
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
|
||||
}
|
||||
return globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||
}
|
||||
|
||||
// Reset BucketVersioningSys to initial state.
|
||||
func (sys *BucketVersioningSys) Reset() {
|
||||
// There is currently no internal state.
|
||||
if bucket == minioMetaBucket || strings.HasPrefix(bucket, minioMetaBucket) {
|
||||
return &versioning.Versioning{XMLNS: "http://s3.amazonaws.com/doc/2006-03-01/"}, nil
|
||||
}
|
||||
|
||||
vcfg, _, err := globalBucketMetadataSys.GetVersioningConfig(bucket)
|
||||
return vcfg, err
|
||||
}
|
||||
|
||||
// NewBucketVersioningSys - creates new versioning system.
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
uatomic "go.uber.org/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
// callhomeSchemaVersion1 is callhome schema version 1
|
||||
callhomeSchemaVersion1 = "1"
|
||||
|
||||
// callhomeSchemaVersion is current callhome schema version.
|
||||
callhomeSchemaVersion = callhomeSchemaVersion1
|
||||
|
||||
// callhomeCycleDefault is the default interval between two callhome cycles (24hrs)
|
||||
callhomeCycleDefault = 24 * time.Hour
|
||||
)
|
||||
|
||||
// CallhomeInfo - Contains callhome information
|
||||
type CallhomeInfo struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
AdminInfo madmin.InfoMessage `json:"admin_info"`
|
||||
}
|
||||
|
||||
var (
|
||||
enableCallhome = uatomic.NewBool(false)
|
||||
callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||
callhomeFreq = uatomic.NewDuration(callhomeCycleDefault)
|
||||
)
|
||||
|
||||
func updateCallhomeParams(ctx context.Context, objAPI ObjectLayer) {
|
||||
alreadyEnabled := enableCallhome.Load()
|
||||
enableCallhome.Store(globalCallhomeConfig.Enable)
|
||||
callhomeFreq.Store(globalCallhomeConfig.Frequency)
|
||||
|
||||
// If callhome was disabled earlier and has now been enabled,
|
||||
// initialize the callhome process again.
|
||||
if !alreadyEnabled && enableCallhome.Load() {
|
||||
initCallhome(ctx, objAPI)
|
||||
}
|
||||
}
|
||||
|
||||
// initCallhome will start the callhome task in the background.
|
||||
func initCallhome(ctx context.Context, objAPI ObjectLayer) {
|
||||
go func() {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
// Leader node (that successfully acquires the lock inside runCallhome)
|
||||
// will keep performing the callhome. If the leader goes down for some reason,
|
||||
// the lock will be released and another node will acquire it and take over
|
||||
// because of this loop.
|
||||
for {
|
||||
runCallhome(ctx, objAPI)
|
||||
if !enableCallhome.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
// callhome running on a different node.
|
||||
// sleep for some time and try again.
|
||||
duration := time.Duration(r.Float64() * float64(callhomeFreq.Load()))
|
||||
if duration < time.Second {
|
||||
// Make sure to sleep atleast a second to avoid high CPU ticks.
|
||||
duration = time.Second
|
||||
}
|
||||
time.Sleep(duration)
|
||||
|
||||
if !enableCallhome.Load() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func runCallhome(ctx context.Context, objAPI ObjectLayer) {
|
||||
// Make sure only 1 callhome is running on the cluster.
|
||||
locker := objAPI.NewNSLock(minioMetaBucket, "callhome/runCallhome.lock")
|
||||
lkctx, err := locker.GetLock(ctx, callhomeLeaderLockTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx = lkctx.Context()
|
||||
defer locker.Unlock(lkctx.Cancel)
|
||||
|
||||
callhomeTimer := time.NewTimer(callhomeFreq.Load())
|
||||
defer callhomeTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-callhomeTimer.C:
|
||||
if !enableCallhome.Load() {
|
||||
// Stop the processing as callhome got disabled
|
||||
return
|
||||
}
|
||||
performCallhome(ctx)
|
||||
|
||||
// Reset the timer for next cycle.
|
||||
callhomeTimer.Reset(callhomeFreq.Load())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func performCallhome(ctx context.Context) {
|
||||
err := sendCallhomeInfo(
|
||||
CallhomeInfo{
|
||||
SchemaVersion: callhomeSchemaVersion,
|
||||
AdminInfo: getServerInfo(ctx, nil),
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -190,6 +190,10 @@ func minioConfigToConsoleFeatures() {
|
||||
os.Setenv("CONSOLE_PROMETHEUS_URL", value)
|
||||
if value := env.Get("MINIO_PROMETHEUS_JOB_ID", "minio-job"); value != "" {
|
||||
os.Setenv("CONSOLE_PROMETHEUS_JOB_ID", value)
|
||||
// Support additional labels for more granular filtering.
|
||||
if value := env.Get("MINIO_PROMETHEUS_EXTRA_LABELS", ""); value != "" {
|
||||
os.Setenv("CONSOLE_PROMETHEUS_EXTRA_LABELS", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Enable if LDAP is enabled.
|
||||
@@ -226,8 +230,8 @@ func minioConfigToConsoleFeatures() {
|
||||
if globalSubnetConfig.APIKey != "" {
|
||||
os.Setenv("CONSOLE_SUBNET_API_KEY", globalSubnetConfig.APIKey)
|
||||
}
|
||||
if globalSubnetConfig.Proxy != "" {
|
||||
os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.Proxy)
|
||||
if globalSubnetConfig.ProxyURL != nil {
|
||||
os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.ProxyURL.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
-13
@@ -28,15 +28,18 @@ import (
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/api"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
"github.com/minio/minio/internal/config/callhome"
|
||||
"github.com/minio/minio/internal/config/compress"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
"github.com/minio/minio/internal/config/heal"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||
"github.com/minio/minio/internal/config/notify"
|
||||
"github.com/minio/minio/internal/config/policy/opa"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/scanner"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/config/subnet"
|
||||
@@ -55,7 +58,9 @@ func initHelp() {
|
||||
config.IdentityLDAPSubSys: xldap.DefaultKVS,
|
||||
config.IdentityOpenIDSubSys: openid.DefaultKVS,
|
||||
config.IdentityTLSSubSys: xtls.DefaultKVS,
|
||||
config.IdentityPluginSubSys: idplugin.DefaultKVS,
|
||||
config.PolicyOPASubSys: opa.DefaultKVS,
|
||||
config.PolicyPluginSubSys: polplugin.DefaultKVS,
|
||||
config.SiteSubSys: config.DefaultSiteKVS,
|
||||
config.RegionSubSys: config.DefaultRegionKVS,
|
||||
config.APISubSys: api.DefaultKVS,
|
||||
@@ -66,6 +71,7 @@ func initHelp() {
|
||||
config.HealSubSys: heal.DefaultKVS,
|
||||
config.ScannerSubSys: scanner.DefaultKVS,
|
||||
config.SubnetSubSys: subnet.DefaultKVS,
|
||||
config.CallhomeSubSys: callhome.DefaultKVS,
|
||||
}
|
||||
for k, v := range notify.DefaultNotificationKVS {
|
||||
kvs[k] = v
|
||||
@@ -107,8 +113,12 @@ func initHelp() {
|
||||
Description: "enable X.509 TLS certificate SSO support",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.PolicyOPASubSys,
|
||||
Description: "[DEPRECATED] enable external OPA for policy enforcement",
|
||||
Key: config.IdentityPluginSubSys,
|
||||
Description: "enable Identity Plugin via external hook",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.PolicyPluginSubSys,
|
||||
Description: "enable Access Management Plugin for policy enforcement",
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.APISubSys,
|
||||
@@ -193,6 +203,12 @@ func initHelp() {
|
||||
Description: "set subnet config for the cluster e.g. api key",
|
||||
Optional: true,
|
||||
},
|
||||
config.HelpKV{
|
||||
Key: config.CallhomeSubSys,
|
||||
Type: "string",
|
||||
Description: "enable callhome for the cluster",
|
||||
Optional: true,
|
||||
},
|
||||
}
|
||||
|
||||
if globalIsErasure {
|
||||
@@ -218,7 +234,9 @@ func initHelp() {
|
||||
config.IdentityOpenIDSubSys: openid.Help,
|
||||
config.IdentityLDAPSubSys: xldap.Help,
|
||||
config.IdentityTLSSubSys: xtls.Help,
|
||||
config.IdentityPluginSubSys: idplugin.Help,
|
||||
config.PolicyOPASubSys: opa.Help,
|
||||
config.PolicyPluginSubSys: polplugin.Help,
|
||||
config.LoggerWebhookSubSys: logger.Help,
|
||||
config.AuditWebhookSubSys: logger.HelpWebhook,
|
||||
config.AuditKafkaSubSys: logger.HelpKafka,
|
||||
@@ -233,6 +251,7 @@ func initHelp() {
|
||||
config.NotifyWebhookSubSys: notify.HelpWebhook,
|
||||
config.NotifyESSubSys: notify.HelpES,
|
||||
config.SubnetSubSys: subnet.HelpSubnet,
|
||||
config.CallhomeSubSys: callhome.HelpCallhome,
|
||||
}
|
||||
|
||||
config.RegisterHelpSubSys(helpMap)
|
||||
@@ -243,6 +262,10 @@ func initHelp() {
|
||||
Key: config.RegionSubSys,
|
||||
Description: "[DEPRECATED - use `site` instead] label the location of the server",
|
||||
},
|
||||
config.PolicyOPASubSys: {
|
||||
Key: config.PolicyOPASubSys,
|
||||
Description: "[DEPRECATED - use `policy_plugin` instead] enable external OPA for policy enforcement",
|
||||
},
|
||||
}
|
||||
|
||||
config.RegisterHelpDeprecatedSubSys(deprecatedHelpKVMap)
|
||||
@@ -335,14 +358,34 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er
|
||||
if _, err := xtls.Lookup(s[config.IdentityTLSSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.IdentityPluginSubSys:
|
||||
if _, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.SubnetSubSys:
|
||||
if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.CallhomeSubSys:
|
||||
if _, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default]); err != nil {
|
||||
return err
|
||||
}
|
||||
case config.PolicyOPASubSys:
|
||||
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
// In case legacy OPA config is being set, we treat it as if the
|
||||
// AuthZPlugin is being set.
|
||||
subSys = config.PolicyPluginSubSys
|
||||
fallthrough
|
||||
case config.PolicyPluginSubSys:
|
||||
if ppargs, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
return err
|
||||
} else if ppargs.URL == nil {
|
||||
// Check if legacy opa is configured.
|
||||
if _, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
if config.LoggerSubSystems.Contains(subSys) {
|
||||
@@ -523,19 +566,39 @@ func lookupConfigs(s config.Config, objAPI ObjectLayer) {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OpenID: %w", err))
|
||||
}
|
||||
|
||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize OPA: %w", err))
|
||||
}
|
||||
globalPolicyOPA = opa.New(opaCfg)
|
||||
|
||||
globalLDAPConfig, err = xldap.Lookup(s[config.IdentityLDAPSubSys][config.Default],
|
||||
globalRootCAs)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse LDAP configuration: %w", err))
|
||||
}
|
||||
|
||||
authNPluginCfg, err := idplugin.LookupConfig(s[config.IdentityPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody, globalSite.Region)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthNPlugin: %w", err))
|
||||
}
|
||||
globalAuthNPlugin = idplugin.New(authNPluginCfg)
|
||||
|
||||
authZPluginCfg, err := polplugin.LookupConfig(s[config.PolicyPluginSubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin: %w", err))
|
||||
}
|
||||
if authZPluginCfg.URL == nil {
|
||||
opaCfg, err := opa.LookupConfig(s[config.PolicyOPASubSys][config.Default],
|
||||
NewGatewayHTTPTransport(), xhttp.DrainBody)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to initialize AuthZPlugin from legacy OPA config: %w", err))
|
||||
} else {
|
||||
authZPluginCfg.URL = opaCfg.URL
|
||||
authZPluginCfg.AuthToken = opaCfg.AuthToken
|
||||
authZPluginCfg.Transport = opaCfg.Transport
|
||||
authZPluginCfg.CloseRespFn = opaCfg.CloseRespFn
|
||||
}
|
||||
}
|
||||
|
||||
setGlobalAuthZPlugin(polplugin.New(authZPluginCfg))
|
||||
|
||||
globalSubnetConfig, err = subnet.LookupConfig(s[config.SubnetSubSys][config.Default])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to parse subnet configuration: %w", err))
|
||||
@@ -599,7 +662,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
return fmt.Errorf("Unable to apply scanner config: %w", err)
|
||||
}
|
||||
// update dynamic scanner values.
|
||||
scannerCycle.Update(scannerCfg.Cycle)
|
||||
scannerCycle.Store(scannerCfg.Cycle)
|
||||
logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait))
|
||||
case config.LoggerWebhookSubSys:
|
||||
loggerCfg, err := logger.LookupConfigForSubSys(s, config.LoggerWebhookSubSys)
|
||||
@@ -609,7 +672,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
userAgent := getUserAgent(getMinioMode())
|
||||
for n, l := range loggerCfg.HTTP {
|
||||
if l.Enabled {
|
||||
l.LogOnce = logger.LogOnceIf
|
||||
l.LogOnce = logger.LogOnceConsoleIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||
loggerCfg.HTTP[n] = l
|
||||
@@ -627,7 +690,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
userAgent := getUserAgent(getMinioMode())
|
||||
for n, l := range loggerCfg.AuditWebhook {
|
||||
if l.Enabled {
|
||||
l.LogOnce = logger.LogOnceIf
|
||||
l.LogOnce = logger.LogOnceConsoleIf
|
||||
l.UserAgent = userAgent
|
||||
l.Transport = NewGatewayHTTPTransportWithClientCerts(l.ClientCert, l.ClientKey)
|
||||
loggerCfg.AuditWebhook[n] = l
|
||||
@@ -669,6 +732,14 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf
|
||||
}
|
||||
}
|
||||
}
|
||||
case config.CallhomeSubSys:
|
||||
callhomeCfg, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default])
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Unable to load callhome config: %w", err))
|
||||
} else {
|
||||
globalCallhomeConfig = callhomeCfg
|
||||
updateCallhomeParams(ctx, objAPI)
|
||||
}
|
||||
}
|
||||
globalServerConfigMu.Lock()
|
||||
defer globalServerConfigMu.Unlock()
|
||||
|
||||
@@ -45,7 +45,7 @@ type HTTPConsoleLoggerSys struct {
|
||||
// NewConsoleLogger - creates new HTTPConsoleLoggerSys with all nodes subscribed to
|
||||
// the console logging pub sub system
|
||||
func NewConsoleLogger(ctx context.Context) *HTTPConsoleLoggerSys {
|
||||
ps := pubsub.New()
|
||||
ps := pubsub.New(8)
|
||||
return &HTTPConsoleLoggerSys{
|
||||
pubsub: ps,
|
||||
console: console.New(),
|
||||
@@ -75,7 +75,7 @@ func (sys *HTTPConsoleLoggerSys) HasLogListeners() bool {
|
||||
}
|
||||
|
||||
// Subscribe starts console logging for this node.
|
||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) {
|
||||
func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan struct{}, node string, last int, logKind string, filter func(entry interface{}) bool) error {
|
||||
// Enable console logging for remote client.
|
||||
if !sys.HasLogListeners() {
|
||||
logger.AddSystemTarget(sys)
|
||||
@@ -111,11 +111,12 @@ func (sys *HTTPConsoleLoggerSys) Subscribe(subCh chan interface{}, doneCh <-chan
|
||||
select {
|
||||
case subCh <- entry:
|
||||
case <-doneCh:
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
sys.pubsub.Subscribe(subCh, doneCh, filter)
|
||||
|
||||
return sys.pubsub.Subscribe(subCh, doneCh, filter)
|
||||
}
|
||||
|
||||
// Init if HTTPConsoleLoggerSys is valid, always returns nil right now
|
||||
|
||||
+17
-27
@@ -45,6 +45,7 @@ import (
|
||||
"github.com/minio/minio/internal/event"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/console"
|
||||
uatomic "go.uber.org/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -66,9 +67,7 @@ var (
|
||||
dataScannerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
|
||||
// Sleeper values are updated when config is loaded.
|
||||
scannerSleeper = newDynamicSleeper(10, 10*time.Second)
|
||||
scannerCycle = &safeDuration{
|
||||
t: dataScannerStartDelay,
|
||||
}
|
||||
scannerCycle = uatomic.NewDuration(dataScannerStartDelay)
|
||||
)
|
||||
|
||||
// initDataScanner will start the scanner in the background.
|
||||
@@ -78,7 +77,7 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
||||
// Run the data scanner in a loop
|
||||
for {
|
||||
runDataScanner(ctx, objAPI)
|
||||
duration := time.Duration(r.Float64() * float64(scannerCycle.Get()))
|
||||
duration := time.Duration(r.Float64() * float64(scannerCycle.Load()))
|
||||
if duration < time.Second {
|
||||
// Make sure to sleep atleast a second to avoid high CPU ticks.
|
||||
duration = time.Second
|
||||
@@ -88,23 +87,6 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) {
|
||||
}()
|
||||
}
|
||||
|
||||
type safeDuration struct {
|
||||
sync.Mutex
|
||||
t time.Duration
|
||||
}
|
||||
|
||||
func (s *safeDuration) Update(t time.Duration) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.t = t
|
||||
}
|
||||
|
||||
func (s *safeDuration) Get() time.Duration {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.t
|
||||
}
|
||||
|
||||
func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode {
|
||||
bitrotCycle := globalHealConfig.BitrotScanCycle()
|
||||
switch bitrotCycle {
|
||||
@@ -189,7 +171,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
||||
}
|
||||
}
|
||||
|
||||
scannerTimer := time.NewTimer(scannerCycle.Get())
|
||||
scannerTimer := time.NewTimer(scannerCycle.Load())
|
||||
defer scannerTimer.Stop()
|
||||
|
||||
for {
|
||||
@@ -197,9 +179,6 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-scannerTimer.C:
|
||||
// Reset the timer for next cycle.
|
||||
scannerTimer.Reset(scannerCycle.Get())
|
||||
|
||||
if intDataUpdateTracker.debug {
|
||||
console.Debugln("starting scanner cycle")
|
||||
}
|
||||
@@ -232,6 +211,9 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the timer for next cycle.
|
||||
scannerTimer.Reset(scannerCycle.Load())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1057,9 +1039,13 @@ func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ Ob
|
||||
fivs = fivs[:lim+1]
|
||||
|
||||
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
||||
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
|
||||
|
||||
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
|
||||
|
||||
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
|
||||
for _, fi := range overflowVersions {
|
||||
obj := fi.ToObjectInfo(i.bucket, i.objectPath())
|
||||
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
|
||||
// skip versions with object locking enabled
|
||||
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
||||
if i.debug {
|
||||
@@ -1184,7 +1170,8 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
||||
opts.VersionID = obj.VersionID
|
||||
}
|
||||
if opts.VersionID == "" {
|
||||
opts.Versioned = globalBucketVersioningSys.Enabled(obj.Bucket)
|
||||
opts.Versioned = globalBucketVersioningSys.PrefixEnabled(obj.Bucket, obj.Name)
|
||||
opts.VersionSuspended = globalBucketVersioningSys.PrefixSuspended(obj.Bucket, obj.Name)
|
||||
}
|
||||
|
||||
obj, err := objLayer.DeleteObject(ctx, obj.Bucket, obj.Name, opts)
|
||||
@@ -1243,6 +1230,9 @@ func (i *scannerItem) objectPath() string {
|
||||
// healReplication will heal a scanned item that has failed replication.
|
||||
func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
|
||||
roi := getHealReplicateObjectInfo(oi, i.replication)
|
||||
if !roi.Dsc.ReplicateAny() {
|
||||
return
|
||||
}
|
||||
|
||||
if oi.DeleteMarker || !oi.VersionPurgeStatus.Empty() {
|
||||
// heal delete marker replication failure or versioned delete replication failure
|
||||
|
||||
@@ -884,6 +884,7 @@ func (c *diskCache) put(ctx context.Context, bucket, object string, data io.Read
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cachePath, 0o777); err != nil {
|
||||
removeAll(cachePath)
|
||||
return oi, err
|
||||
}
|
||||
metadata := cloneMSS(opts.UserDefined)
|
||||
@@ -892,6 +893,7 @@ func (c *diskCache) put(ctx context.Context, bucket, object string, data io.Read
|
||||
if globalCacheKMS != nil {
|
||||
reader, err = newCacheEncryptReader(data, bucket, object, metadata)
|
||||
if err != nil {
|
||||
removeAll(cachePath)
|
||||
return oi, err
|
||||
}
|
||||
actualSize, _ = sio.EncryptedSize(uint64(size))
|
||||
@@ -1628,8 +1630,6 @@ func (c *diskCache) cleanupStaleUploads(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
// Reset for the next interval
|
||||
timer.Reset(cacheStaleUploadCleanupInterval)
|
||||
now := time.Now()
|
||||
readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir), func(shaDir string, typ os.FileMode) error {
|
||||
return readDirFn(pathJoin(c.dir, minioMetaBucket, cacheMultipartDir, shaDir), func(uploadIDDir string, typ os.FileMode) error {
|
||||
@@ -1660,6 +1660,9 @@ func (c *diskCache) cleanupStaleUploads(ctx context.Context) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Reset for the next interval
|
||||
timer.Reset(cacheStaleUploadCleanupInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-16
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
"github.com/minio/minio/internal/disk"
|
||||
"github.com/minio/minio/internal/hash"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
"github.com/minio/pkg/wildcard"
|
||||
@@ -739,35 +738,30 @@ func (c *cacheObjects) PutObject(ctx context.Context, bucket, object string, r *
|
||||
// Initialize pipe to stream data to cache
|
||||
rPipe, wPipe := io.Pipe()
|
||||
infoCh := make(chan ObjectInfo)
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
defer close(infoCh)
|
||||
info, err := putObjectFn(ctx, bucket, object, NewPutObjReader(hashReader), opts)
|
||||
if err != nil {
|
||||
close(infoCh)
|
||||
pipeReader.CloseWithError(err)
|
||||
rPipe.CloseWithError(err)
|
||||
errorCh <- err
|
||||
return
|
||||
pipeReader.CloseWithError(err)
|
||||
rPipe.CloseWithError(err)
|
||||
if err == nil {
|
||||
infoCh <- info
|
||||
}
|
||||
close(errorCh)
|
||||
infoCh <- info
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_, err := dcache.put(lkctx.Context(), bucket, object, rPipe, r.Size(), nil, opts, false, false)
|
||||
if err != nil {
|
||||
rPipe.CloseWithError(err)
|
||||
return
|
||||
logger.LogIf(lkctx.Context(), err)
|
||||
}
|
||||
// We do not care about errors to cached backend.
|
||||
rPipe.Close()
|
||||
}()
|
||||
|
||||
mwriter := cacheMultiWriter(pipeWriter, wPipe)
|
||||
_, err = io.Copy(mwriter, r)
|
||||
pipeWriter.Close()
|
||||
wPipe.Close()
|
||||
|
||||
if err != nil {
|
||||
err = <-errorCh
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
info := <-infoCh
|
||||
@@ -803,8 +797,7 @@ func (c *cacheObjects) uploadObject(ctx context.Context, oi ObjectInfo) {
|
||||
return
|
||||
}
|
||||
var opts ObjectOptions
|
||||
opts.UserDefined = make(map[string]string)
|
||||
opts.UserDefined[xhttp.ContentMD5] = oi.UserDefined["content-md5"]
|
||||
opts.UserDefined = cloneMSS(oi.UserDefined)
|
||||
objInfo, err := c.InnerPutObjectFn(ctx, oi.Bucket, oi.Name, NewPutObjReader(hashReader), opts)
|
||||
wbCommitStatus := CommitComplete
|
||||
size := objInfo.Size
|
||||
|
||||
+1
-1
@@ -582,7 +582,7 @@ func CreateEndpoints(serverAddr string, foundLocal bool, args ...[]string) (Endp
|
||||
return endpoints, setupType, config.ErrInvalidFSEndpoint(nil).Msg("use path style endpoint for FS setup")
|
||||
}
|
||||
endpoints = append(endpoints, endpoint)
|
||||
setupType = FSSetupType
|
||||
setupType = ErasureSDSetupType
|
||||
|
||||
// Check for cross device mounts if any.
|
||||
if err = checkCrossDeviceMounts(endpoints); err != nil {
|
||||
|
||||
@@ -231,10 +231,10 @@ func TestCreateEndpoints(t *testing.T) {
|
||||
}{
|
||||
{"localhost", [][]string{}, "", Endpoints{}, -1, fmt.Errorf("address localhost: missing port in address")},
|
||||
|
||||
// FS Setup
|
||||
// Erasure Single Drive
|
||||
{"localhost:9000", [][]string{{"http://localhost/d1"}}, "", Endpoints{}, -1, fmt.Errorf("use path style endpoint for FS setup")},
|
||||
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
|
||||
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, FSSetupType, nil},
|
||||
{":443", [][]string{{"/d1"}}, ":443", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
|
||||
{"localhost:10000", [][]string{{"/d1"}}, "localhost:10000", Endpoints{Endpoint{URL: &url.URL{Path: mustAbs("/d1")}, IsLocal: true}}, ErasureSDSetupType, nil},
|
||||
{"localhost:9000", [][]string{{"https://127.0.0.1:9000/d1", "https://localhost:9001/d1", "https://example.com/d1", "https://example.com/d2"}}, "", Endpoints{}, -1, fmt.Errorf("path '/d1' can not be served by different port on same address")},
|
||||
|
||||
// Erasure Setup with PathEndpointType
|
||||
|
||||
@@ -41,7 +41,7 @@ type Erasure struct {
|
||||
// NewErasure creates a new ErasureStorage.
|
||||
func NewErasure(ctx context.Context, dataBlocks, parityBlocks int, blockSize int64) (e Erasure, err error) {
|
||||
// Check the parameters for sanity now.
|
||||
if dataBlocks <= 0 || parityBlocks <= 0 {
|
||||
if dataBlocks <= 0 || parityBlocks < 0 {
|
||||
return e, reedsolomon.ErrInvShardNum
|
||||
}
|
||||
|
||||
|
||||
+26
-10
@@ -418,10 +418,19 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
||||
// When disk to heal count is greater than parity blocks we should simply error out.
|
||||
err := fmt.Errorf("more disks are expected to heal than parity, returned errors: %v -> %s/%s(%s)", errs, bucket, object, versionID)
|
||||
logger.LogIf(ctx, err)
|
||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||
bucket, object, versionID), err
|
||||
}
|
||||
|
||||
cleanFileInfo := func(fi FileInfo) FileInfo {
|
||||
// Returns a copy of the 'fi' with checksums and parts nil'ed.
|
||||
// Returns a copy of the 'fi' with erasure index, checksums and inline data niled.
|
||||
nfi := fi
|
||||
if !fi.IsRemote() {
|
||||
if !nfi.IsRemote() {
|
||||
nfi.Data = nil
|
||||
nfi.Erasure.Index = 0
|
||||
nfi.Erasure.Checksums = nil
|
||||
}
|
||||
@@ -432,12 +441,25 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
tmpID := mustGetUUID()
|
||||
migrateDataDir := mustGetUUID()
|
||||
|
||||
// Reorder so that we have data disks first and parity disks next.
|
||||
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
||||
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
||||
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
||||
|
||||
copyPartsMetadata := make([]FileInfo, len(partsMetadata))
|
||||
for i := range latestDisks {
|
||||
if latestDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
copyPartsMetadata[i] = partsMetadata[i]
|
||||
}
|
||||
|
||||
for i := range outDatedDisks {
|
||||
if outDatedDisks[i] == nil {
|
||||
continue
|
||||
}
|
||||
copyPartsMetadata[i] = partsMetadata[i]
|
||||
// Make sure to write the FileInfo information
|
||||
// that is expected to be in quorum.
|
||||
partsMetadata[i] = cleanFileInfo(latestMeta)
|
||||
}
|
||||
|
||||
@@ -456,12 +478,6 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
inlineBuffers = make([]*bytes.Buffer, len(outDatedDisks))
|
||||
}
|
||||
|
||||
// Reorder so that we have data disks first and parity disks next.
|
||||
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
||||
outDatedDisks = shuffleDisks(outDatedDisks, latestMeta.Erasure.Distribution)
|
||||
partsMetadata = shufflePartsMetadata(partsMetadata, latestMeta.Erasure.Distribution)
|
||||
copyPartsMetadata = shufflePartsMetadata(copyPartsMetadata, latestMeta.Erasure.Distribution)
|
||||
|
||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||
// Heal each part. erasureHealFile() will write the healed
|
||||
// part to .minio/tmp/uuid/ which needs to be renamed later to
|
||||
@@ -486,7 +502,7 @@ func (er erasureObjects) healObject(ctx context.Context, bucket string, object s
|
||||
}
|
||||
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
||||
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||
readers[i] = newBitrotReader(disk, partsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||
checksumInfo.Hash, erasure.ShardSize())
|
||||
}
|
||||
writers := make([]io.Writer, len(outDatedDisks))
|
||||
|
||||
@@ -137,24 +137,29 @@ func readAllFileInfo(ctx context.Context, disks []StorageAPI, bucket, object, ve
|
||||
return errDiskNotFound
|
||||
}
|
||||
metadataArray[index], err = disks[index].ReadVersion(ctx, bucket, object, versionID, readData)
|
||||
if err != nil {
|
||||
if !IsErr(err, []error{
|
||||
errFileNotFound,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], bucket, object, err),
|
||||
disks[index].String())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}, index)
|
||||
}
|
||||
|
||||
errs := g.Wait()
|
||||
for index, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !IsErr(err, []error{
|
||||
errFileNotFound,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], bucket, object, err),
|
||||
disks[index].String())
|
||||
}
|
||||
}
|
||||
|
||||
// Return all the metadata.
|
||||
return metadataArray, g.Wait()
|
||||
return metadataArray, errs
|
||||
}
|
||||
|
||||
// shuffleDisksAndPartsMetadataByIndex this function should be always used by GetObjectNInfo()
|
||||
@@ -178,10 +183,6 @@ func shuffleDisksAndPartsMetadataByIndex(disks []StorageAPI, metaArr []FileInfo,
|
||||
inconsistent++
|
||||
continue
|
||||
}
|
||||
if len(fi.Data) != len(meta.Data) {
|
||||
inconsistent++
|
||||
continue
|
||||
}
|
||||
if meta.XLV1 != fi.XLV1 {
|
||||
inconsistent++
|
||||
continue
|
||||
@@ -229,12 +230,6 @@ func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo,
|
||||
// if object was ever written previously.
|
||||
continue
|
||||
}
|
||||
if !init && len(fi.Data) != len(partsMetadata[index].Data) {
|
||||
// Check for length of data parts only when
|
||||
// fi.ModTime is not empty - ModTime is always set,
|
||||
// if object was ever written previously.
|
||||
continue
|
||||
}
|
||||
if !init && fi.XLV1 != partsMetadata[index].XLV1 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/bucket/replication"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/minio/internal/sync/errgroup"
|
||||
@@ -99,15 +99,15 @@ func (fi FileInfo) IsValid() bool {
|
||||
fi.Erasure.Index <= dataBlocks+parityBlocks &&
|
||||
len(fi.Erasure.Distribution) == (dataBlocks+parityBlocks))
|
||||
return ((dataBlocks >= parityBlocks) &&
|
||||
(dataBlocks != 0) && (parityBlocks != 0) &&
|
||||
(dataBlocks > 0) && (parityBlocks >= 0) &&
|
||||
correctIndexes)
|
||||
}
|
||||
|
||||
// ToObjectInfo - Converts metadata to object info.
|
||||
func (fi FileInfo) ToObjectInfo(bucket, object string) ObjectInfo {
|
||||
func (fi FileInfo) ToObjectInfo(bucket, object string, versioned bool) ObjectInfo {
|
||||
object = decodeDirObject(object)
|
||||
versionID := fi.VersionID
|
||||
if (globalBucketVersioningSys.Enabled(bucket) || globalBucketVersioningSys.Suspended(bucket)) && versionID == "" {
|
||||
if versioned && versionID == "" {
|
||||
versionID = nullVersionID
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func (fi FileInfo) ObjectToPartOffset(ctx context.Context, offset int64) (partIn
|
||||
|
||||
func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.Time, quorum int) (FileInfo, error) {
|
||||
// with less quorum return error.
|
||||
if quorum < 2 {
|
||||
if quorum < 1 {
|
||||
return FileInfo{}, errErasureReadQuorum
|
||||
}
|
||||
metaHashes := make([]string, len(metaArr))
|
||||
@@ -297,8 +297,6 @@ func findFileInfoInQuorum(ctx context.Context, metaArr []FileInfo, modTime time.
|
||||
fmt.Fprintf(h, "part.%d", part.Number)
|
||||
}
|
||||
fmt.Fprintf(h, "%v", meta.Erasure.Distribution)
|
||||
// make sure that length of Data is same
|
||||
fmt.Fprintf(h, "%v", len(meta.Data))
|
||||
|
||||
// ILM transition fields
|
||||
fmt.Fprint(h, meta.TransitionStatus)
|
||||
@@ -400,6 +398,10 @@ func writeUniqueFileInfo(ctx context.Context, disks []StorageAPI, bucket, prefix
|
||||
// readQuorum is the min required disks to read data.
|
||||
// writeQuorum is the min required disks to write data.
|
||||
func objectQuorumFromMeta(ctx context.Context, partsMetaData []FileInfo, errs []error, defaultParityCount int) (objectReadQuorum, objectWriteQuorum int, err error) {
|
||||
if defaultParityCount == 0 {
|
||||
return 1, 1, nil
|
||||
}
|
||||
|
||||
// get the latest updated Metadata and a count of all the latest updated FileInfo(s)
|
||||
latestFileInfo, err := getLatestFileInfo(ctx, partsMetaData, errs)
|
||||
if err != nil {
|
||||
|
||||
+22
-10
@@ -190,6 +190,8 @@ func (er erasureObjects) cleanupStaleUploadsOnDisk(ctx context.Context, disk Sto
|
||||
// towards simplification of multipart APIs.
|
||||
// The resulting ListMultipartsInfo structure is unmarshalled directly as XML.
|
||||
func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, object, keyMarker, uploadIDMarker, delimiter string, maxUploads int) (result ListMultipartsInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
result.MaxUploads = maxUploads
|
||||
result.KeyMarker = keyMarker
|
||||
result.Prefix = object
|
||||
@@ -284,8 +286,10 @@ func (er erasureObjects) ListMultipartUploads(ctx context.Context, bucket, objec
|
||||
// disks. `uploads.json` carries metadata regarding on-going multipart
|
||||
// operation(s) on the object.
|
||||
func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string, object string, opts ObjectOptions) (string, error) {
|
||||
userDefined := cloneMSS(opts.UserDefined)
|
||||
|
||||
onlineDisks := er.getDisks()
|
||||
parityDrives := globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
||||
parityDrives := globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives <= 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
@@ -306,7 +310,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
}
|
||||
if parityOrig != parityDrives {
|
||||
opts.UserDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||
userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||
}
|
||||
|
||||
dataDrives := len(onlineDisks) - parityDrives
|
||||
@@ -334,8 +338,8 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
}
|
||||
|
||||
// Guess content-type from the extension if possible.
|
||||
if opts.UserDefined["content-type"] == "" {
|
||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
if userDefined["content-type"] == "" {
|
||||
userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
@@ -350,7 +354,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
for index := range partsMetadata {
|
||||
partsMetadata[index].Fresh = true
|
||||
partsMetadata[index].ModTime = modTime
|
||||
partsMetadata[index].Metadata = opts.UserDefined
|
||||
partsMetadata[index].Metadata = userDefined
|
||||
}
|
||||
|
||||
uploadID := mustGetUUID()
|
||||
@@ -371,10 +375,8 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
||||
//
|
||||
// Implements S3 compatible initiate multipart API.
|
||||
func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (string, error) {
|
||||
// No metadata is set, allocate a new one.
|
||||
if opts.UserDefined == nil {
|
||||
opts.UserDefined = make(map[string]string)
|
||||
}
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
return er.newMultipartUpload(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
@@ -446,6 +448,8 @@ func renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, ds
|
||||
//
|
||||
// Implements S3 compatible Upload Part API.
|
||||
func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, r *PutObjReader, opts ObjectOptions) (pi PartInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
// Write lock for this part ID.
|
||||
// Held throughout the operation.
|
||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||
@@ -683,6 +687,8 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
||||
// - encrypted
|
||||
// - compressed
|
||||
func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (MultipartInfo, error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
result := MultipartInfo{
|
||||
Bucket: bucket,
|
||||
Object: object,
|
||||
@@ -739,6 +745,8 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
|
||||
// ListPartsInfo structure is marshaled directly into XML and
|
||||
// replied back to the client.
|
||||
func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
@@ -832,6 +840,8 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
||||
//
|
||||
// Implements S3 compatible Complete multipart API.
|
||||
func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, parts []CompletePart, opts ObjectOptions) (oi ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
// Hold read-locks to verify uploaded parts, also disallows
|
||||
// parallel part uploads as well.
|
||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
@@ -1017,7 +1027,7 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
fi.IsLatest = true
|
||||
|
||||
// Success, return object info.
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
// AbortMultipartUpload - aborts an ongoing multipart operation
|
||||
@@ -1028,6 +1038,8 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
||||
// would be removed from the system, rollback is not possible on this
|
||||
// operation.
|
||||
func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||
if err != nil {
|
||||
|
||||
+108
-51
@@ -63,6 +63,8 @@ func countOnlineDisks(onlineDisks []StorageAPI) (online int) {
|
||||
// if source object and destination object are same we only
|
||||
// update metadata.
|
||||
func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, dstBucket, dstObject string, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions) (oi ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, dstObject, &er)
|
||||
|
||||
// This call shouldn't be used for anything other than metadata updates or adding self referential versions.
|
||||
if !srcInfo.metadataOnly {
|
||||
return oi, NotImplemented{}
|
||||
@@ -115,7 +117,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
if srcOpts.VersionID == "" {
|
||||
return oi, toObjectErr(errFileNotFound, srcBucket, srcObject)
|
||||
}
|
||||
return fi.ToObjectInfo(srcBucket, srcObject), toObjectErr(errMethodNotAllowed, srcBucket, srcObject)
|
||||
return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), toObjectErr(errMethodNotAllowed, srcBucket, srcObject)
|
||||
}
|
||||
|
||||
filterOnlineDisksInplace(fi, metaArr, onlineDisks)
|
||||
@@ -131,15 +133,6 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
modTime = UTCNow()
|
||||
}
|
||||
|
||||
// If the data is not inlined, we may end up incorrectly
|
||||
// inlining the data here, that leads to an inconsistent
|
||||
// situation where some objects are were not inlined
|
||||
// were now inlined, make sure to `nil` the Data such
|
||||
// that xl.meta is written as expected.
|
||||
if !fi.InlineData() {
|
||||
fi.Data = nil
|
||||
}
|
||||
|
||||
fi.VersionID = versionID // set any new versionID we might have created
|
||||
fi.ModTime = modTime // set modTime for the new versionID
|
||||
if !dstOpts.MTime.IsZero() {
|
||||
@@ -149,12 +142,15 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
fi.Metadata = srcInfo.UserDefined
|
||||
srcInfo.UserDefined["etag"] = srcInfo.ETag
|
||||
|
||||
inlineData := fi.InlineData()
|
||||
freeVersionID := fi.TierFreeVersionID()
|
||||
freeVersionMarker := fi.TierFreeVersion()
|
||||
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range metaArr {
|
||||
if metaArr[index].IsValid() {
|
||||
metaArr[index].ModTime = modTime
|
||||
metaArr[index].VersionID = versionID
|
||||
metaArr[index].Metadata = srcInfo.UserDefined
|
||||
if !metaArr[index].InlineData() {
|
||||
// If the data is not inlined, we may end up incorrectly
|
||||
// inlining the data here, that leads to an inconsistent
|
||||
@@ -163,6 +159,17 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
// that xl.meta is written as expected.
|
||||
metaArr[index].Data = nil
|
||||
}
|
||||
metaArr[index].Metadata = srcInfo.UserDefined
|
||||
// Preserve existing values
|
||||
if inlineData {
|
||||
metaArr[index].SetInlineData()
|
||||
}
|
||||
if freeVersionID != "" {
|
||||
metaArr[index].SetTierFreeVersionID(freeVersionID)
|
||||
}
|
||||
if freeVersionMarker {
|
||||
metaArr[index].SetTierFreeVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,12 +178,14 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
||||
return oi, toObjectErr(err, srcBucket, srcObject)
|
||||
}
|
||||
|
||||
return fi.ToObjectInfo(srcBucket, srcObject), nil
|
||||
return fi.ToObjectInfo(srcBucket, srcObject, srcOpts.Versioned || srcOpts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
// GetObjectNInfo - returns object info and an object
|
||||
// Read(Closer). When err != nil, the returned reader is always nil.
|
||||
func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
var unlockOnDefer bool
|
||||
nsUnlocker := func() {}
|
||||
defer func() {
|
||||
@@ -236,7 +245,7 @@ func (er erasureObjects) GetObjectNInfo(ctx context.Context, bucket, object stri
|
||||
}
|
||||
}
|
||||
|
||||
objInfo := fi.ToObjectInfo(bucket, object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
if objInfo.DeleteMarker {
|
||||
if opts.VersionID == "" {
|
||||
return &GetObjectReader{
|
||||
@@ -413,6 +422,8 @@ func (er erasureObjects) getObjectWithFileInfo(ctx context.Context, bucket, obje
|
||||
|
||||
// GetObjectInfo - reads object metadata and replies back ObjectInfo.
|
||||
func (er erasureObjects) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (info ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
if !opts.NoLock {
|
||||
// Lock the object before reading.
|
||||
lk := er.NewNSLock(bucket, object)
|
||||
@@ -465,16 +476,6 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
}
|
||||
rf, err := disks[index].ReadXL(ctx, bucket, object, readData)
|
||||
if err != nil {
|
||||
if !IsErr(err, []error{
|
||||
errFileNotFound,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], bucket, object, err),
|
||||
disks[index].String())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -491,6 +492,22 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
}
|
||||
|
||||
errs := g.Wait()
|
||||
for index, err := range errs {
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !IsErr(err, []error{
|
||||
errFileNotFound,
|
||||
errVolumeNotFound,
|
||||
errFileVersionNotFound,
|
||||
errDiskNotFound,
|
||||
}...) {
|
||||
logger.LogOnceIf(ctx, fmt.Errorf("Drive %s, path (%s/%s) returned an error (%w)",
|
||||
disks[index], bucket, object, err),
|
||||
disks[index].String())
|
||||
}
|
||||
}
|
||||
|
||||
for index := range metadataArray {
|
||||
if metadataArray[index] != nil {
|
||||
metadataShallowVersions[index] = metadataArray[index].versions
|
||||
@@ -498,23 +515,45 @@ func readAllXL(ctx context.Context, disks []StorageAPI, bucket, object string, r
|
||||
}
|
||||
|
||||
readQuorum := (len(disks) + 1) / 2
|
||||
merged := mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)
|
||||
meta := &xlMetaV2{versions: mergeXLV2Versions(readQuorum, false, 1, metadataShallowVersions...)}
|
||||
lfi, err := meta.ToFileInfo(bucket, object, "")
|
||||
if err != nil {
|
||||
for i := range errs {
|
||||
if errs[i] == nil {
|
||||
errs[i] = err
|
||||
}
|
||||
}
|
||||
return metaFileInfos, errs
|
||||
}
|
||||
if !lfi.IsValid() {
|
||||
for i := range errs {
|
||||
if errs[i] == nil {
|
||||
errs[i] = errCorruptedFormat
|
||||
}
|
||||
}
|
||||
return metaFileInfos, errs
|
||||
}
|
||||
|
||||
for index := range metadataArray {
|
||||
if metadataArray[index] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataArray[index].versions = merged
|
||||
|
||||
// make sure to preserve this for diskmtime based healing bugfix.
|
||||
diskMTime := metaFileInfos[index].DiskMTime
|
||||
metaFileInfos[index], errs[index] = metadataArray[index].ToFileInfo(bucket, object, "")
|
||||
if errs[index] == nil {
|
||||
if errs[index] != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metaFileInfos[index].IsValid() && metaFileInfos[index].ModTime.Equal(lfi.ModTime) {
|
||||
versionID := metaFileInfos[index].VersionID
|
||||
if versionID == "" {
|
||||
versionID = nullVersionID
|
||||
}
|
||||
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
|
||||
if readData {
|
||||
metaFileInfos[index].Data = metadataArray[index].data.find(versionID)
|
||||
}
|
||||
metaFileInfos[index].DiskMTime = diskMTime
|
||||
}
|
||||
}
|
||||
@@ -582,6 +621,8 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
||||
if metaArr[i].IsValid() && metaArr[i].ModTime.Equal(fi.ModTime) {
|
||||
continue
|
||||
}
|
||||
metaArr[i] = FileInfo{}
|
||||
onlineDisks[i] = nil
|
||||
missingBlocks++
|
||||
}
|
||||
|
||||
@@ -603,7 +644,7 @@ func (er erasureObjects) getObjectInfo(ctx context.Context, bucket, object strin
|
||||
if err != nil {
|
||||
return objInfo, toObjectErr(err, bucket, object)
|
||||
}
|
||||
objInfo = fi.ToObjectInfo(bucket, object)
|
||||
objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
if fi.Deleted {
|
||||
if opts.VersionID == "" || opts.DeleteMarker {
|
||||
return objInfo, toObjectErr(errFileNotFound, bucket, object)
|
||||
@@ -627,7 +668,7 @@ func (er erasureObjects) getObjectInfoAndQuorum(ctx context.Context, bucket, obj
|
||||
wquorum++
|
||||
}
|
||||
|
||||
objInfo = fi.ToObjectInfo(bucket, object)
|
||||
objInfo = fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
if !fi.VersionPurgeStatus().Empty() && opts.VersionID != "" {
|
||||
// Make sure to return object info to provide extra information.
|
||||
return objInfo, wquorum, toObjectErr(errMethodNotAllowed, bucket, object)
|
||||
@@ -819,7 +860,7 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
||||
return ObjectInfo{}, toObjectErr(err, minioMetaBucket, key)
|
||||
}
|
||||
|
||||
return fi.ToObjectInfo(minioMetaBucket, key), nil
|
||||
return fi.ToObjectInfo(minioMetaBucket, key, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
// PutObject - creates an object upon reading from the input stream
|
||||
@@ -832,19 +873,18 @@ func (er erasureObjects) PutObject(ctx context.Context, bucket string, object st
|
||||
|
||||
// putObject wrapper for erasureObjects PutObject
|
||||
func (er erasureObjects) putObject(ctx context.Context, bucket string, object string, r *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
data := r.Reader
|
||||
|
||||
// No metadata is set, allocate a new one.
|
||||
if opts.UserDefined == nil {
|
||||
opts.UserDefined = make(map[string]string)
|
||||
}
|
||||
userDefined := cloneMSS(opts.UserDefined)
|
||||
|
||||
storageDisks := er.getDisks()
|
||||
|
||||
parityDrives := len(storageDisks) / 2
|
||||
if !opts.MaxParity {
|
||||
// Get parity and data drive count based on storage class metadata
|
||||
parityDrives = globalStorageClass.GetParityForSC(opts.UserDefined[xhttp.AmzStorageClass])
|
||||
parityDrives = globalStorageClass.GetParityForSC(userDefined[xhttp.AmzStorageClass])
|
||||
if parityDrives <= 0 {
|
||||
parityDrives = er.defaultParityCount
|
||||
}
|
||||
@@ -882,7 +922,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
parityDrives = len(storageDisks) / 2
|
||||
}
|
||||
if parityOrig != parityDrives {
|
||||
opts.UserDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||
userDefined[minIOErasureUpgraded] = strconv.Itoa(parityOrig) + "->" + strconv.Itoa(parityDrives)
|
||||
}
|
||||
}
|
||||
dataDrives := len(storageDisks) - parityDrives
|
||||
@@ -1058,13 +1098,13 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
Hash: bitrotWriterSum(w),
|
||||
})
|
||||
}
|
||||
if opts.UserDefined["etag"] == "" {
|
||||
opts.UserDefined["etag"] = r.MD5CurrentHexString()
|
||||
if userDefined["etag"] == "" {
|
||||
userDefined["etag"] = r.MD5CurrentHexString()
|
||||
}
|
||||
|
||||
// Guess content-type from the extension if possible.
|
||||
if opts.UserDefined["content-type"] == "" {
|
||||
opts.UserDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
if userDefined["content-type"] == "" {
|
||||
userDefined["content-type"] = mimedb.TypeByExtension(path.Ext(object))
|
||||
}
|
||||
|
||||
modTime := opts.MTime
|
||||
@@ -1075,7 +1115,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
// Fill all the necessary metadata.
|
||||
// Update `xl.meta` content on each disks.
|
||||
for index := range partsMetadata {
|
||||
partsMetadata[index].Metadata = opts.UserDefined
|
||||
partsMetadata[index].Metadata = userDefined
|
||||
partsMetadata[index].Size = n
|
||||
partsMetadata[index].ModTime = modTime
|
||||
}
|
||||
@@ -1125,7 +1165,7 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
||||
// we are adding a new version to this object under the namespace lock, so this is the latest version.
|
||||
fi.IsLatest = true
|
||||
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object string, writeQuorum int, fi FileInfo, forceDelMarker bool) error {
|
||||
@@ -1148,6 +1188,10 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
|
||||
// into smaller bulks if some object names are found to be duplicated in the delete list, splitting
|
||||
// into smaller bulks will avoid holding twice the write lock of the duplicated object names.
|
||||
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
|
||||
for _, obj := range objects {
|
||||
auditObjectErasureSet(ctx, obj.ObjectV.ObjectName, &er)
|
||||
}
|
||||
|
||||
errs := make([]error, len(objects))
|
||||
dobjects := make([]DeletedObject, len(objects))
|
||||
writeQuorums := make([]int, len(objects))
|
||||
@@ -1178,7 +1222,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
// VersionID is not set means delete is not specific about
|
||||
// any version, look for if the bucket is versioned or not.
|
||||
if objects[i].VersionID == "" {
|
||||
if opts.Versioned || opts.VersionSuspended {
|
||||
// MinIO extension to bucket version configuration
|
||||
suspended := opts.VersionSuspended
|
||||
versioned := opts.Versioned
|
||||
if opts.PrefixEnabledFn != nil {
|
||||
versioned = opts.PrefixEnabledFn(objects[i].ObjectName)
|
||||
}
|
||||
if versioned || suspended {
|
||||
// Bucket is versioned and no version was explicitly
|
||||
// mentioned for deletes, create a delete marker instead.
|
||||
vr.ModTime = UTCNow()
|
||||
@@ -1186,7 +1236,7 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
// Versioning suspended means that we add a `null` version
|
||||
// delete marker, if not add a new version for this delete
|
||||
// marker.
|
||||
if opts.Versioned {
|
||||
if versioned {
|
||||
vr.VersionID = mustGetUUID()
|
||||
}
|
||||
}
|
||||
@@ -1307,12 +1357,17 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
||||
func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string) error {
|
||||
disks := er.getDisks()
|
||||
g := errgroup.WithNErrs(len(disks))
|
||||
dirPrefix := encodeDirObject(prefix)
|
||||
for index := range disks {
|
||||
index := index
|
||||
g.Go(func() error {
|
||||
if disks[index] == nil {
|
||||
return nil
|
||||
}
|
||||
// Deletes
|
||||
// - The prefix and its children
|
||||
// - The prefix__XLDIR__
|
||||
defer disks[index].Delete(ctx, bucket, dirPrefix, true)
|
||||
return disks[index].Delete(ctx, bucket, prefix, true)
|
||||
}, index)
|
||||
}
|
||||
@@ -1328,6 +1383,8 @@ func (er erasureObjects) deletePrefix(ctx context.Context, bucket, prefix string
|
||||
// any error as it is not necessary for the handler to reply back a
|
||||
// response to the client request.
|
||||
func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
auditObjectErasureSet(ctx, object, &er)
|
||||
|
||||
if opts.DeletePrefix {
|
||||
return ObjectInfo{}, toObjectErr(er.deletePrefix(ctx, bucket, object), bucket, object)
|
||||
}
|
||||
@@ -1482,7 +1539,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
||||
if err = er.deleteObjectVersion(ctx, bucket, object, writeQuorum, fi, opts.DeleteMarker); err != nil {
|
||||
return objInfo, toObjectErr(err, bucket, object)
|
||||
}
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1587,7 +1644,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
opts.VersionID = fi.VersionID
|
||||
}
|
||||
|
||||
objInfo := fi.ToObjectInfo(bucket, object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
if opts.EvalMetadataFn != nil {
|
||||
if err := opts.EvalMetadataFn(objInfo); err != nil {
|
||||
return ObjectInfo{}, err
|
||||
@@ -1603,7 +1660,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
// PutObjectTags - replace or add tags to an existing object
|
||||
@@ -1667,7 +1724,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
||||
return ObjectInfo{}, toObjectErr(err, bucket, object)
|
||||
}
|
||||
|
||||
return fi.ToObjectInfo(bucket, object), nil
|
||||
return fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended), nil
|
||||
}
|
||||
|
||||
// updateObjectMeta will update the metadata of a file.
|
||||
@@ -1805,7 +1862,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
||||
break
|
||||
}
|
||||
|
||||
objInfo := fi.ToObjectInfo(bucket, object)
|
||||
objInfo := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
sendEvent(eventArgs{
|
||||
EventName: eventName,
|
||||
BucketName: bucket,
|
||||
@@ -1860,7 +1917,7 @@ func (er erasureObjects) restoreTransitionedObject(ctx context.Context, bucket s
|
||||
return setRestoreHeaderFn(oi, toObjectErr(err, bucket, object))
|
||||
}
|
||||
|
||||
oi = actualfi.ToObjectInfo(bucket, object)
|
||||
oi = actualfi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
ropts := putRestoreOpts(bucket, object, opts.Transition.RestoreRequest, oi)
|
||||
if len(oi.Parts) == 1 {
|
||||
var rs *HTTPRangeSpec
|
||||
|
||||
@@ -20,11 +20,14 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -1048,3 +1051,157 @@ func testObjectQuorumFromMeta(obj ObjectLayer, instanceType string, dirs []strin
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// In some deployments, one object has data inlined in one disk and not inlined in other disks.
|
||||
func TestGetObjectInlineNotInline(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create a backend with 4 disks named disk{1...4}, this name convention
|
||||
// because we will unzip some object data from a sample archive.
|
||||
const numDisks = 4
|
||||
path, err := ioutil.TempDir(globalTestTmpDir, "minio-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var fsDirs []string
|
||||
for i := 1; i <= numDisks; i++ {
|
||||
fsDirs = append(fsDirs, filepath.Join(path, fmt.Sprintf("disk%d", i)))
|
||||
}
|
||||
|
||||
objLayer, _, err := initObjectLayer(ctx, mustGetPoolEndpoints(fsDirs...))
|
||||
if err != nil {
|
||||
removeRoots(fsDirs)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cleaning up of temporary test directories
|
||||
defer objLayer.Shutdown(context.Background())
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
// Create a testbucket
|
||||
err = objLayer.MakeBucketWithLocation(ctx, "testbucket", BucketOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Unzip sample object data to the existing disks
|
||||
err = unzipArchive("testdata/xl-meta-inline-notinline.zip", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Try to read the object and check its md5sum
|
||||
gr, err := objLayer.GetObjectNInfo(ctx, "testbucket", "file", nil, nil, readLock, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, gr)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GetObject reading data to succeed, but failed with %v", err)
|
||||
}
|
||||
gr.Close()
|
||||
|
||||
const expectedHash = "fffb6377948ebea75ad2b8058e849ef5"
|
||||
foundHash := fmt.Sprintf("%x", h.Sum(nil))
|
||||
if foundHash != expectedHash {
|
||||
t.Fatalf("Expected data to have md5sum = `%s`, found `%s`", expectedHash, foundHash)
|
||||
}
|
||||
}
|
||||
|
||||
// Test reading an object with some outdated data in some disks
|
||||
func TestGetObjectWithOutdatedDisks(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create an instance of xl backend.
|
||||
obj, fsDirs, err := prepareErasure(ctx, 6)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Cleanup backend directories.
|
||||
defer obj.Shutdown(context.Background())
|
||||
defer removeRoots(fsDirs)
|
||||
|
||||
z := obj.(*erasureServerPools)
|
||||
sets := z.serverPools[0]
|
||||
xl := sets.sets[0]
|
||||
|
||||
origErasureDisks := xl.getDisks()
|
||||
|
||||
testCases := []struct {
|
||||
bucket string
|
||||
versioned bool
|
||||
object string
|
||||
content []byte
|
||||
}{
|
||||
{"bucket1", false, "object1", []byte("aaaaaaaaaaaaaaaa")},
|
||||
{"bucket2", false, "object2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
|
||||
{"bucket3", true, "version1", []byte("aaaaaaaaaaaaaaaa")},
|
||||
{"bucket4", true, "version2", bytes.Repeat([]byte{'a'}, smallFileThreshold*2)},
|
||||
}
|
||||
|
||||
for i, testCase := range testCases {
|
||||
// Step 1: create a bucket
|
||||
err = z.MakeBucketWithLocation(ctx, testCase.bucket, BucketOptions{VersioningEnabled: testCase.versioned})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to create a bucket: %v", i+1, err)
|
||||
}
|
||||
|
||||
// Step 2: Upload an object with a random content
|
||||
initialData := bytes.Repeat([]byte{'b'}, len(testCase.content))
|
||||
sets.erasureDisksMu.Lock()
|
||||
xl.getDisks = func() []StorageAPI { return origErasureDisks }
|
||||
sets.erasureDisksMu.Unlock()
|
||||
_, err = z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(initialData), int64(len(initialData)), "", ""),
|
||||
ObjectOptions{Versioned: testCase.versioned})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to upload a random object: %v", i+1, err)
|
||||
}
|
||||
|
||||
// Step 3: Upload the object with some disks offline
|
||||
sets.erasureDisksMu.Lock()
|
||||
xl.getDisks = func() []StorageAPI {
|
||||
disks := make([]StorageAPI, len(origErasureDisks))
|
||||
copy(disks, origErasureDisks)
|
||||
disks[0] = nil
|
||||
disks[1] = nil
|
||||
return disks
|
||||
}
|
||||
sets.erasureDisksMu.Unlock()
|
||||
_, err = z.PutObject(ctx, testCase.bucket, testCase.object, mustGetPutObjReader(t, bytes.NewReader(testCase.content), int64(len(testCase.content)), "", ""),
|
||||
ObjectOptions{Versioned: testCase.versioned})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to upload the final object: %v", i+1, err)
|
||||
}
|
||||
|
||||
// Step 4: Try to read the object back and check its md5sum
|
||||
sets.erasureDisksMu.Lock()
|
||||
xl.getDisks = func() []StorageAPI { return origErasureDisks }
|
||||
sets.erasureDisksMu.Unlock()
|
||||
gr, err := z.GetObjectNInfo(ctx, testCase.bucket, testCase.object, nil, nil, readLock, ObjectOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Expected GetObject to succeed, but failed with %v", err)
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
h.Write(testCase.content)
|
||||
expectedHash := h.Sum(nil)
|
||||
|
||||
h.Reset()
|
||||
_, err = io.Copy(h, gr)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Failed to calculate md5sum of the object: %v", i+1, err)
|
||||
}
|
||||
gr.Close()
|
||||
foundHash := h.Sum(nil)
|
||||
|
||||
if !bytes.Equal(foundHash, expectedHash) {
|
||||
t.Fatalf("Test %d: Expected data to have md5sum = `%x`, found `%x`", i+1, expectedHash, foundHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,39 +242,24 @@ func (p *poolMeta) Decommission(idx int, pi poolSpaceInfo) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Return an error when there is decommission on going - the user needs
|
||||
// to explicitly cancel it first in order to restart decommissioning again.
|
||||
if p.Pools[idx].Decommission != nil &&
|
||||
!p.Pools[idx].Decommission.Complete &&
|
||||
!p.Pools[idx].Decommission.Failed &&
|
||||
!p.Pools[idx].Decommission.Canceled {
|
||||
return errDecommissionAlreadyRunning
|
||||
}
|
||||
|
||||
now := UTCNow()
|
||||
if p.Pools[idx].Decommission == nil {
|
||||
p.Pools[idx].LastUpdate = now
|
||||
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
||||
StartTime: now,
|
||||
StartSize: pi.Free,
|
||||
CurrentSize: pi.Free,
|
||||
TotalSize: pi.Total,
|
||||
}
|
||||
return nil
|
||||
p.Pools[idx].LastUpdate = now
|
||||
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
||||
StartTime: now,
|
||||
StartSize: pi.Free,
|
||||
CurrentSize: pi.Free,
|
||||
TotalSize: pi.Total,
|
||||
}
|
||||
|
||||
// Completed pool doesn't need to be decommissioned again.
|
||||
if p.Pools[idx].Decommission.Complete {
|
||||
return errDecommissionComplete
|
||||
}
|
||||
|
||||
// Canceled or Failed decommission can be triggered again.
|
||||
if p.Pools[idx].Decommission.StartTime.IsZero() {
|
||||
if p.Pools[idx].Decommission.Canceled || p.Pools[idx].Decommission.Failed {
|
||||
p.Pools[idx].LastUpdate = now
|
||||
p.Pools[idx].Decommission = &PoolDecommissionInfo{
|
||||
StartTime: now,
|
||||
StartSize: pi.Free,
|
||||
CurrentSize: pi.Free,
|
||||
TotalSize: pi.Total,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
} // In-progress pool doesn't need to be decommissioned again.
|
||||
|
||||
// In all other scenarios an active decommissioning is in progress.
|
||||
return errDecommissionAlreadyRunning
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p poolMeta) IsSuspended(idx int) bool {
|
||||
@@ -556,8 +541,13 @@ func (z *erasureServerPools) Init(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) decommissionObject(ctx context.Context, bucket string, gr *GetObjectReader) (err error) {
|
||||
defer gr.Close()
|
||||
objInfo := gr.ObjInfo
|
||||
|
||||
defer func() {
|
||||
gr.Close()
|
||||
auditLogDecom(ctx, "DecomCopyData", objInfo.Bucket, objInfo.Name, objInfo.VersionID, err)
|
||||
}()
|
||||
|
||||
if objInfo.isMultipart() {
|
||||
uploadID, err := z.NewMultipartUpload(ctx, bucket, objInfo.Name, ObjectOptions{
|
||||
VersionID: objInfo.VersionID,
|
||||
@@ -618,6 +608,8 @@ func (v versionsSorter) reverse() {
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool *erasureSets, bName string) error {
|
||||
ctx = logger.SetReqInfo(ctx, &logger.ReqInfo{})
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wStr := env.Get("_MINIO_DECOMMISSION_WORKERS", strconv.Itoa(len(pool.sets)))
|
||||
workerSize, err := strconv.Atoi(wStr)
|
||||
@@ -627,7 +619,6 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
|
||||
parallelWorkers := make(chan struct{}, workerSize)
|
||||
|
||||
versioned := globalBucketVersioningSys.Enabled(bName)
|
||||
for _, set := range pool.sets {
|
||||
set := set
|
||||
disks := set.getOnlineDisks()
|
||||
@@ -637,6 +628,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
continue
|
||||
}
|
||||
|
||||
vc, _ := globalBucketVersioningSys.Get(bName)
|
||||
decommissionEntry := func(entry metaCacheEntry) {
|
||||
defer func() {
|
||||
<-parallelWorkers
|
||||
@@ -675,7 +667,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
bName,
|
||||
version.Name,
|
||||
ObjectOptions{
|
||||
Versioned: versioned,
|
||||
Versioned: vc.PrefixEnabled(version.Name),
|
||||
VersionID: version.VersionID,
|
||||
MTime: version.ModTime,
|
||||
DeleteReplication: version.ReplicationState,
|
||||
@@ -695,27 +687,34 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
continue
|
||||
}
|
||||
|
||||
gr, err := set.GetObjectNInfo(ctx,
|
||||
bName,
|
||||
encodeDirObject(version.Name),
|
||||
nil,
|
||||
http.Header{},
|
||||
noLock, // all mutations are blocked reads are safe without locks.
|
||||
ObjectOptions{
|
||||
VersionID: version.VersionID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
z.poolMetaMutex.Lock()
|
||||
z.poolMeta.CountItem(idx, version.Size, true)
|
||||
z.poolMetaMutex.Unlock()
|
||||
break // break out on first error
|
||||
}
|
||||
var failure bool
|
||||
// gr.Close() is ensured by decommissionObject().
|
||||
if err = z.decommissionObject(ctx, bName, gr); err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
failure = true
|
||||
for try := 0; try < 3; try++ {
|
||||
gr, err := set.GetObjectNInfo(ctx,
|
||||
bName,
|
||||
encodeDirObject(version.Name),
|
||||
nil,
|
||||
http.Header{},
|
||||
noLock, // all mutations are blocked reads are safe without locks.
|
||||
ObjectOptions{
|
||||
VersionID: version.VersionID,
|
||||
})
|
||||
if isErrObjectNotFound(err) {
|
||||
// object deleted by the application, nothing to do here we move on.
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
failure = true
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
if err = z.decommissionObject(ctx, bName, gr); err != nil {
|
||||
failure = true
|
||||
logger.LogIf(ctx, err)
|
||||
continue
|
||||
}
|
||||
failure = false
|
||||
break
|
||||
}
|
||||
z.poolMetaMutex.Lock()
|
||||
z.poolMeta.CountItem(idx, version.Size, failure)
|
||||
@@ -728,13 +727,17 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
||||
|
||||
// if all versions were decommissioned, then we can delete the object versions.
|
||||
if decommissionedCount == len(fivs.Versions) {
|
||||
set.DeleteObject(ctx,
|
||||
_, err := set.DeleteObject(ctx,
|
||||
bName,
|
||||
entry.name,
|
||||
encodeDirObject(entry.name),
|
||||
ObjectOptions{
|
||||
DeletePrefix: true, // use prefix delete to delete all versions at once.
|
||||
},
|
||||
)
|
||||
auditLogDecom(ctx, "DecomDeleteObject", bName, entry.name, "", err)
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
z.poolMetaMutex.Lock()
|
||||
z.poolMeta.TrackCurrentBucketObject(idx, bName, entry.name)
|
||||
@@ -819,6 +822,9 @@ func (z *erasureServerPools) doDecommissionInRoutine(ctx context.Context, idx in
|
||||
dctx, z.decommissionCancelers[idx] = context.WithCancel(GlobalContext)
|
||||
z.poolMetaMutex.Unlock()
|
||||
|
||||
// Generate an empty request info so it can be directly modified later by audit
|
||||
dctx = logger.SetReqInfo(dctx, &logger.ReqInfo{})
|
||||
|
||||
if err := z.decommissionInBackground(dctx, idx); err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
logger.LogIf(GlobalContext, z.DecommissionFailed(dctx, idx))
|
||||
@@ -1090,3 +1096,16 @@ func (z *erasureServerPools) StartDecommission(ctx context.Context, idx int) (er
|
||||
globalNotificationSys.ReloadPoolMeta(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func auditLogDecom(ctx context.Context, apiName, bucket, object, versionID string, err error) {
|
||||
errStr := ""
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
auditLogInternal(ctx, bucket, object, AuditLogOptions{
|
||||
Trigger: "decommissioning",
|
||||
APIName: apiName,
|
||||
VersionID: versionID,
|
||||
Error: errStr,
|
||||
})
|
||||
}
|
||||
|
||||
+92
-11
@@ -61,6 +61,22 @@ func (z *erasureServerPools) SinglePool() bool {
|
||||
|
||||
// Initialize new pool of erasure sets.
|
||||
func newErasureServerPools(ctx context.Context, endpointServerPools EndpointServerPools) (ObjectLayer, error) {
|
||||
if endpointServerPools.NEndpoints() == 1 {
|
||||
ep := endpointServerPools[0]
|
||||
storageDisks, format, err := waitForFormatErasure(true, ep.Endpoints, 1, ep.SetCount, ep.DrivesPerSet, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objLayer, err := newErasureSingle(ctx, storageDisks[0], format)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
globalLocalDrives = storageDisks
|
||||
return objLayer, nil
|
||||
}
|
||||
|
||||
var (
|
||||
deploymentID string
|
||||
distributionAlgo string
|
||||
@@ -233,8 +249,9 @@ func (z *erasureServerPools) SetDriveCounts() []int {
|
||||
type serverPoolsAvailableSpace []poolAvailableSpace
|
||||
|
||||
type poolAvailableSpace struct {
|
||||
Index int
|
||||
Available uint64
|
||||
Index int
|
||||
Available uint64
|
||||
MaxUsedPct int // Used disk percentage of most filled disk, rounded down.
|
||||
}
|
||||
|
||||
// TotalAvailable - total available space
|
||||
@@ -246,10 +263,41 @@ func (p serverPoolsAvailableSpace) TotalAvailable() uint64 {
|
||||
return total
|
||||
}
|
||||
|
||||
// FilterMaxUsed will filter out any pools that has used percent bigger than max,
|
||||
// unless all have that, in which case all are preserved.
|
||||
func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
||||
// We aren't modifying p, only entries in it, so we don't need to receive a pointer.
|
||||
if len(p) <= 1 {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
for _, z := range p {
|
||||
if z.MaxUsedPct < max {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
// All above limit.
|
||||
// Do not modify
|
||||
return
|
||||
}
|
||||
|
||||
// Remove entries that are above.
|
||||
for i, z := range p {
|
||||
if z.MaxUsedPct < max {
|
||||
continue
|
||||
}
|
||||
p[i].Available = 0
|
||||
}
|
||||
}
|
||||
|
||||
// getAvailablePoolIdx will return an index that can hold size bytes.
|
||||
// -1 is returned if no serverPools have available space for the size given.
|
||||
func (z *erasureServerPools) getAvailablePoolIdx(ctx context.Context, bucket, object string, size int64) int {
|
||||
serverPools := z.getServerPoolsAvailableSpace(ctx, bucket, object, size)
|
||||
serverPools.FilterMaxUsed(100 - (100 * diskReserveFraction))
|
||||
total := serverPools.TotalAvailable()
|
||||
if total == 0 {
|
||||
return -1
|
||||
@@ -270,11 +318,13 @@ func (z *erasureServerPools) getAvailablePoolIdx(ctx context.Context, bucket, ob
|
||||
|
||||
// getServerPoolsAvailableSpace will return the available space of each pool after storing the content.
|
||||
// If there is not enough space the pool will return 0 bytes available.
|
||||
// The size of each will be multiplied by the number of sets.
|
||||
// Negative sizes are seen as 0 bytes.
|
||||
func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, bucket, object string, size int64) serverPoolsAvailableSpace {
|
||||
serverPools := make(serverPoolsAvailableSpace, len(z.serverPools))
|
||||
|
||||
storageInfos := make([][]*DiskInfo, len(z.serverPools))
|
||||
nSets := make([]int, len(z.serverPools))
|
||||
g := errgroup.WithNErrs(len(z.serverPools))
|
||||
for index := range z.serverPools {
|
||||
index := index
|
||||
@@ -282,9 +332,11 @@ func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, b
|
||||
if z.IsSuspended(index) {
|
||||
continue
|
||||
}
|
||||
pool := z.serverPools[index]
|
||||
nSets[index] = pool.setCount
|
||||
g.Go(func() error {
|
||||
// Get the set where it would be placed.
|
||||
storageInfos[index] = getDiskInfos(ctx, z.serverPools[index].getHashedSet(object).getDisks())
|
||||
storageInfos[index] = getDiskInfos(ctx, pool.getHashedSet(object).getDisks()...)
|
||||
return nil
|
||||
}, index)
|
||||
}
|
||||
@@ -298,15 +350,30 @@ func (z *erasureServerPools) getServerPoolsAvailableSpace(ctx context.Context, b
|
||||
serverPools[i] = poolAvailableSpace{Index: i}
|
||||
continue
|
||||
}
|
||||
var maxUsedPct int
|
||||
for _, disk := range zinfo {
|
||||
if disk == nil {
|
||||
if disk == nil || disk.Total == 0 {
|
||||
continue
|
||||
}
|
||||
available += disk.Total - disk.Used
|
||||
|
||||
// set maxUsedPct to the value from the disk with the least space percentage.
|
||||
if pctUsed := int(disk.Used * 100 / disk.Total); pctUsed > maxUsedPct {
|
||||
maxUsedPct = pctUsed
|
||||
}
|
||||
}
|
||||
|
||||
// Since we are comparing pools that may have a different number of sets
|
||||
// we multiply by the number of sets in the pool.
|
||||
// This will compensate for differences in set sizes
|
||||
// when choosing destination pool.
|
||||
// Different set sizes are already compensated by less disks.
|
||||
available *= uint64(nSets[i])
|
||||
|
||||
serverPools[i] = poolAvailableSpace{
|
||||
Index: i,
|
||||
Available: available,
|
||||
Index: i,
|
||||
Available: available,
|
||||
MaxUsedPct: maxUsedPct,
|
||||
}
|
||||
}
|
||||
return serverPools
|
||||
@@ -774,7 +841,16 @@ func (z *erasureServerPools) GetObjectNInfo(ctx context.Context, bucket, object
|
||||
}
|
||||
|
||||
lockType = noLock // do not take locks at lower levels for GetObjectNInfo()
|
||||
return z.serverPools[zIdx].GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
gr, err = z.serverPools[zIdx].GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if unlockOnDefer {
|
||||
unlockOnDefer = false
|
||||
return gr.WithCleanupFuncs(nsUnlocker), nil
|
||||
}
|
||||
return gr, nil
|
||||
}
|
||||
|
||||
// getLatestObjectInfoWithIdx returns the objectInfo of the latest object from multiple pools (this function
|
||||
@@ -873,7 +949,7 @@ func (z *erasureServerPools) PutObject(ctx context.Context, bucket string, objec
|
||||
object = encodeDirObject(object)
|
||||
|
||||
if z.SinglePool() {
|
||||
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()), data.Size()) {
|
||||
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()...), data.Size()) {
|
||||
return ObjectInfo{}, toObjectErr(errDiskFull)
|
||||
}
|
||||
return z.serverPools[0].PutObject(ctx, bucket, object, data, opts)
|
||||
@@ -1265,7 +1341,7 @@ func (z *erasureServerPools) NewMultipartUpload(ctx context.Context, bucket, obj
|
||||
}
|
||||
|
||||
if z.SinglePool() {
|
||||
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()), -1) {
|
||||
if !isMinioMetaBucketName(bucket) && !hasSpaceFor(getDiskInfos(ctx, z.serverPools[0].getHashedSet(object).getDisks()...), -1) {
|
||||
return "", toObjectErr(errDiskFull)
|
||||
}
|
||||
return z.serverPools[0].NewMultipartUpload(ctx, bucket, object, opts)
|
||||
@@ -1675,6 +1751,8 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
||||
return err
|
||||
}
|
||||
|
||||
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
go func() {
|
||||
defer cancel()
|
||||
@@ -1707,12 +1785,15 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
||||
if opts.WalkAscending {
|
||||
for i := len(fivs.Versions) - 1; i >= 0; i-- {
|
||||
version := fivs.Versions[i]
|
||||
results <- version.ToObjectInfo(bucket, version.Name)
|
||||
versioned := vcfg != nil && vcfg.Versioned(version.Name)
|
||||
|
||||
results <- version.ToObjectInfo(bucket, version.Name, versioned)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, version := range fivs.Versions {
|
||||
results <- version.ToObjectInfo(bucket, version.Name)
|
||||
versioned := vcfg != nil && vcfg.Versioned(version.Name)
|
||||
results <- version.ToObjectInfo(bucket, version.Name, versioned)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-30
@@ -301,14 +301,14 @@ func (s *erasureSets) monitorAndConnectEndpoints(ctx context.Context, monitorInt
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-monitor.C:
|
||||
// Reset the timer once fired for required interval.
|
||||
monitor.Reset(monitorInterval)
|
||||
|
||||
if serverDebugLog {
|
||||
console.Debugln("running disk monitoring")
|
||||
}
|
||||
|
||||
s.connectDisks()
|
||||
|
||||
// Reset the timer for next interval
|
||||
monitor.Reset(monitorInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +485,9 @@ func newErasureSets(ctx context.Context, endpoints PoolEndpoints, storageDisks [
|
||||
go s.cleanupDeletedObjects(ctx)
|
||||
|
||||
// Start the disk monitoring and connect routine.
|
||||
go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
|
||||
if !globalIsTesting {
|
||||
go s.monitorAndConnectEndpoints(ctx, defaultMonitorConnectEndpointInterval)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -502,9 +504,6 @@ func (s *erasureSets) cleanupDeletedObjects(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
// Reset for the next interval
|
||||
timer.Reset(globalAPIConfig.getDeleteCleanupInterval())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, set := range s.sets {
|
||||
wg.Add(1)
|
||||
@@ -517,6 +516,9 @@ func (s *erasureSets) cleanupDeletedObjects(ctx context.Context) {
|
||||
}(set)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Reset for the next interval
|
||||
timer.Reset(globalAPIConfig.getDeleteCleanupInterval())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -530,9 +532,6 @@ func (s *erasureSets) cleanupStaleUploads(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
// Reset for the next interval
|
||||
timer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, set := range s.sets {
|
||||
wg.Add(1)
|
||||
@@ -545,6 +544,9 @@ func (s *erasureSets) cleanupStaleUploads(ctx context.Context) {
|
||||
}(set)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Reset for the next interval
|
||||
timer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,6 +574,7 @@ func (a *auditObjectErasureMap) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(mapCopy)
|
||||
}
|
||||
|
||||
// Add erasure set information to the current context
|
||||
func auditObjectErasureSet(ctx context.Context, object string, set *erasureObjects) {
|
||||
if len(logger.AuditTargets()) == 0 {
|
||||
return
|
||||
@@ -933,21 +936,18 @@ func (s *erasureSets) ListBuckets(ctx context.Context) (buckets []BucketInfo, er
|
||||
// GetObjectNInfo - returns object info and locked object ReadCloser
|
||||
func (s *erasureSets) GetObjectNInfo(ctx context.Context, bucket, object string, rs *HTTPRangeSpec, h http.Header, lockType LockType, opts ObjectOptions) (gr *GetObjectReader, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts)
|
||||
}
|
||||
|
||||
// PutObject - writes an object to hashedSet based on the object name.
|
||||
func (s *erasureSets) PutObject(ctx context.Context, bucket string, object string, data *PutObjReader, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.PutObject(ctx, bucket, object, data, opts)
|
||||
}
|
||||
|
||||
// GetObjectInfo - reads object metadata from the hashedSet based on the object name.
|
||||
func (s *erasureSets) GetObjectInfo(ctx context.Context, bucket, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.GetObjectInfo(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
@@ -967,13 +967,11 @@ func (s *erasureSets) deletePrefix(ctx context.Context, bucket string, prefix st
|
||||
|
||||
// DeleteObject - deletes an object from the hashedSet based on the object name.
|
||||
func (s *erasureSets) DeleteObject(ctx context.Context, bucket string, object string, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
|
||||
if opts.DeletePrefix {
|
||||
err := s.deletePrefix(ctx, bucket, object)
|
||||
return ObjectInfo{}, err
|
||||
}
|
||||
set := s.getHashedSet(object)
|
||||
return set.DeleteObject(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
@@ -1030,9 +1028,6 @@ func (s *erasureSets) DeleteObjects(ctx context.Context, bucket string, objects
|
||||
for i, obj := range group {
|
||||
delErrs[obj.origIndex] = errs[i]
|
||||
delObjects[obj.origIndex] = dobjects[i]
|
||||
if errs[i] == nil {
|
||||
auditObjectErasureSet(ctx, obj.object.ObjectName, set)
|
||||
}
|
||||
}
|
||||
}(s.sets[setIdx], objsGroup)
|
||||
}
|
||||
@@ -1046,8 +1041,6 @@ func (s *erasureSets) CopyObject(ctx context.Context, srcBucket, srcObject, dstB
|
||||
srcSet := s.getHashedSet(srcObject)
|
||||
dstSet := s.getHashedSet(dstObject)
|
||||
|
||||
auditObjectErasureSet(ctx, dstObject, dstSet)
|
||||
|
||||
cpSrcDstSame := srcSet == dstSet
|
||||
// Check if this request is only metadata update.
|
||||
if cpSrcDstSame && srcInfo.metadataOnly {
|
||||
@@ -1086,14 +1079,12 @@ func (s *erasureSets) ListMultipartUploads(ctx context.Context, bucket, prefix,
|
||||
// In list multipart uploads we are going to treat input prefix as the object,
|
||||
// this means that we are not supporting directory navigation.
|
||||
set := s.getHashedSet(prefix)
|
||||
auditObjectErasureSet(ctx, prefix, set)
|
||||
return set.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads)
|
||||
}
|
||||
|
||||
// Initiate a new multipart upload on a hashedSet based on object name.
|
||||
func (s *erasureSets) NewMultipartUpload(ctx context.Context, bucket, object string, opts ObjectOptions) (uploadID string, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.NewMultipartUpload(ctx, bucket, object, opts)
|
||||
}
|
||||
|
||||
@@ -1102,42 +1093,36 @@ func (s *erasureSets) CopyObjectPart(ctx context.Context, srcBucket, srcObject,
|
||||
startOffset int64, length int64, srcInfo ObjectInfo, srcOpts, dstOpts ObjectOptions,
|
||||
) (partInfo PartInfo, err error) {
|
||||
destSet := s.getHashedSet(destObject)
|
||||
auditObjectErasureSet(ctx, destObject, destSet)
|
||||
return destSet.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, NewPutObjReader(srcInfo.Reader), dstOpts)
|
||||
}
|
||||
|
||||
// PutObjectPart - writes part of an object to hashedSet based on the object name.
|
||||
func (s *erasureSets) PutObjectPart(ctx context.Context, bucket, object, uploadID string, partID int, data *PutObjReader, opts ObjectOptions) (info PartInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts)
|
||||
}
|
||||
|
||||
// GetMultipartInfo - return multipart metadata info uploaded at hashedSet.
|
||||
func (s *erasureSets) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) (result MultipartInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
|
||||
// ListObjectParts - lists all uploaded parts to an object in hashedSet.
|
||||
func (s *erasureSets) ListObjectParts(ctx context.Context, bucket, object, uploadID string, partNumberMarker int, maxParts int, opts ObjectOptions) (result ListPartsInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||
}
|
||||
|
||||
// Aborts an in-progress multipart operation on hashedSet based on the object name.
|
||||
func (s *erasureSets) AbortMultipartUpload(ctx context.Context, bucket, object, uploadID string, opts ObjectOptions) error {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||
}
|
||||
|
||||
// CompleteMultipartUpload - completes a pending multipart transaction, on hashedSet based on object name.
|
||||
func (s *erasureSets) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []CompletePart, opts ObjectOptions) (objInfo ObjectInfo, err error) {
|
||||
set := s.getHashedSet(object)
|
||||
auditObjectErasureSet(ctx, object, set)
|
||||
return set.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||
}
|
||||
|
||||
@@ -1267,7 +1252,7 @@ func (s *erasureSets) HealFormat(ctx context.Context, dryRun bool) (res madmin.H
|
||||
|
||||
defer func(storageDisks []StorageAPI) {
|
||||
if err != nil {
|
||||
closeStorageDisks(storageDisks)
|
||||
closeStorageDisks(storageDisks...)
|
||||
}
|
||||
}(storageDisks)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -79,7 +79,7 @@ func (er erasureObjects) NewNSLock(bucket string, objects ...string) RWLocker {
|
||||
// Shutdown function for object storage interface.
|
||||
func (er erasureObjects) Shutdown(ctx context.Context) error {
|
||||
// Add any object layer shutdown activities here.
|
||||
closeStorageDisks(er.getDisks())
|
||||
closeStorageDisks(er.getDisks()...)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -40,6 +40,9 @@ const (
|
||||
// Represents Erasure backend.
|
||||
formatBackendErasure = "xl"
|
||||
|
||||
// Represents Erasure backend - single drive
|
||||
formatBackendErasureSingle = "xl-single"
|
||||
|
||||
// formatErasureV1.Erasure.Version - version '1'.
|
||||
formatErasureVersionV1 = "1"
|
||||
|
||||
@@ -146,6 +149,9 @@ func newFormatErasureV3(numSets int, setLen int) *formatErasureV3 {
|
||||
format := &formatErasureV3{}
|
||||
format.Version = formatMetaVersionV1
|
||||
format.Format = formatBackendErasure
|
||||
if setLen == 1 {
|
||||
format.Format = formatBackendErasureSingle
|
||||
}
|
||||
format.ID = mustGetUUID()
|
||||
format.Erasure.Version = formatErasureVersionV3
|
||||
format.Erasure.DistributionAlgo = formatErasureVersionV3DistributionAlgoV3
|
||||
@@ -170,8 +176,8 @@ func formatGetBackendErasureVersion(b []byte) (string, error) {
|
||||
if meta.Version != formatMetaVersionV1 {
|
||||
return "", fmt.Errorf(`format.Version expected: %s, got: %s`, formatMetaVersionV1, meta.Version)
|
||||
}
|
||||
if meta.Format != formatBackendErasure {
|
||||
return "", fmt.Errorf(`found backend type %s, expected %s`, meta.Format, formatBackendErasure)
|
||||
if meta.Format != formatBackendErasure && meta.Format != formatBackendErasureSingle {
|
||||
return "", fmt.Errorf(`found backend type %s, expected %s or %s`, meta.Format, formatBackendErasure, formatBackendErasureSingle)
|
||||
}
|
||||
// Erasure backend found, proceed to detect version.
|
||||
format := &formatErasureVersionDetect{}
|
||||
@@ -291,7 +297,7 @@ func formatErasureMigrateV2ToV3(data []byte, export, version string) ([]byte, er
|
||||
func countErrs(errs []error, err error) int {
|
||||
i := 0
|
||||
for _, err1 := range errs {
|
||||
if err1 == err {
|
||||
if err1 == err || errors.Is(err1, err) {
|
||||
i++
|
||||
}
|
||||
}
|
||||
@@ -410,7 +416,7 @@ func checkFormatErasureValue(formatErasure *formatErasureV3, disk StorageAPI) er
|
||||
if formatErasure.Version != formatMetaVersionV1 {
|
||||
return fmt.Errorf("Unsupported version of backend format [%s] found on %s", formatErasure.Version, disk)
|
||||
}
|
||||
if formatErasure.Format != formatBackendErasure {
|
||||
if formatErasure.Format != formatBackendErasure && formatErasure.Format != formatBackendErasureSingle {
|
||||
return fmt.Errorf("Unsupported backend format [%s] found on %s", formatErasure.Format, disk)
|
||||
}
|
||||
if formatErasure.Erasure.Version != formatErasureVersionV3 {
|
||||
@@ -643,7 +649,7 @@ func saveFormatErasureAll(ctx context.Context, storageDisks []StorageAPI, format
|
||||
}
|
||||
|
||||
// relinquishes the underlying connection for all storage disks.
|
||||
func closeStorageDisks(storageDisks []StorageAPI) {
|
||||
func closeStorageDisks(storageDisks ...StorageAPI) {
|
||||
var wg sync.WaitGroup
|
||||
for _, disk := range storageDisks {
|
||||
if disk == nil {
|
||||
|
||||
@@ -240,6 +240,9 @@ func initFormatFS(ctx context.Context, fsPath string) (rlk *lock.RLockedFile, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if formatBackend == formatBackendErasureSingle {
|
||||
return nil, errFreshDisk
|
||||
}
|
||||
if formatBackend != formatBackendFS {
|
||||
return nil, fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
|
||||
}
|
||||
@@ -319,6 +322,10 @@ func formatFSFixDeploymentID(ctx context.Context, fsFormatPath string) error {
|
||||
rlk.Close()
|
||||
return err
|
||||
}
|
||||
if formatBackend == formatBackendErasureSingle {
|
||||
rlk.Close()
|
||||
return errFreshDisk
|
||||
}
|
||||
if formatBackend != formatBackendFS {
|
||||
rlk.Close()
|
||||
return fmt.Errorf(`%s file: expected format-type: %s, found: %s`, formatConfigFile, formatBackendFS, formatBackend)
|
||||
|
||||
@@ -45,6 +45,8 @@ func TestFSV1MetadataObjInfo(t *testing.T) {
|
||||
|
||||
// TestReadFSMetadata - readFSMetadata testing with a healthy and faulty disk
|
||||
func TestReadFSMetadata(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
@@ -80,6 +82,7 @@ func TestReadFSMetadata(t *testing.T) {
|
||||
|
||||
// TestWriteFSMetadata - tests of writeFSMetadata with healthy disk.
|
||||
func TestWriteFSMetadata(t *testing.T) {
|
||||
t.Skip()
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
|
||||
@@ -887,8 +887,6 @@ func (fs *FSObjects) cleanupStaleUploads(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-bgAppendTmpCleaner.C:
|
||||
bgAppendTmpCleaner.Reset(bgAppendsCleanupInterval)
|
||||
|
||||
foundUploadIDs := fs.getAllUploadIDs(ctx)
|
||||
|
||||
// Remove background append map from the memory
|
||||
@@ -915,10 +913,8 @@ func (fs *FSObjects) cleanupStaleUploads(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
bgAppendTmpCleaner.Reset(bgAppendsCleanupInterval)
|
||||
case <-expiryUploadsTimer.C:
|
||||
// Reset for the next interval
|
||||
expiryUploadsTimer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
|
||||
|
||||
expiry := globalAPIConfig.getStaleUploadsExpiry()
|
||||
now := time.Now()
|
||||
|
||||
@@ -944,6 +940,9 @@ func (fs *FSObjects) cleanupStaleUploads(ctx context.Context) {
|
||||
fs.appendFileMapMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset for the next interval
|
||||
expiryUploadsTimer.Reset(globalAPIConfig.getStaleUploadsCleanupInterval())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
|
||||
// Tests cleanup multipart uploads for filesystem backend.
|
||||
func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
t.Skip()
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
@@ -88,6 +89,7 @@ func TestFSCleanupMultipartUploadsInRoutine(t *testing.T) {
|
||||
|
||||
// TestNewMultipartUploadFaultyDisk - test NewMultipartUpload with faulty disks
|
||||
func TestNewMultipartUploadFaultyDisk(t *testing.T) {
|
||||
t.Skip()
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
@@ -142,6 +142,11 @@ func NewFSObjectLayer(fsPath string) (ObjectLayer, error) {
|
||||
return nil, config.ErrUnableToWriteInBackend(err).Hint(hint)
|
||||
}
|
||||
|
||||
fsFormatPath := pathJoin(fsPath, minioMetaBucket, formatConfigFile)
|
||||
if _, err = fsStat(ctx, fsFormatPath); err != nil && os.IsNotExist(err) {
|
||||
return nil, errFreshDisk
|
||||
}
|
||||
|
||||
// Assign a new UUID for FS minio mode. Each server instance
|
||||
// gets its own UUID for temporary file transaction.
|
||||
fsUUID := mustGetUUID()
|
||||
|
||||
@@ -51,6 +51,8 @@ func TestNewFS(t *testing.T) {
|
||||
// TestFSShutdown - initialize a new FS object layer then calls
|
||||
// Shutdown to check returned results
|
||||
func TestFSShutdown(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
bucketName := "testbucket"
|
||||
objectName := "object"
|
||||
// Create and return an fsObject with its path in the disk
|
||||
@@ -83,6 +85,8 @@ func TestFSShutdown(t *testing.T) {
|
||||
|
||||
// TestFSGetBucketInfo - test GetBucketInfo with healty and faulty disks
|
||||
func TestFSGetBucketInfo(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
// Prepare for testing
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
@@ -165,6 +169,7 @@ func TestFSPutObject(t *testing.T) {
|
||||
|
||||
// TestFSDeleteObject - test fs.DeleteObject() with healthy and corrupted disks
|
||||
func TestFSDeleteObject(t *testing.T) {
|
||||
t.Skip()
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
@@ -209,6 +214,7 @@ func TestFSDeleteObject(t *testing.T) {
|
||||
|
||||
// TestFSDeleteBucket - tests for fs DeleteBucket
|
||||
func TestFSDeleteBucket(t *testing.T) {
|
||||
t.Skip()
|
||||
// Prepare for testing
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
@@ -249,6 +255,7 @@ func TestFSDeleteBucket(t *testing.T) {
|
||||
|
||||
// TestFSListBuckets - tests for fs ListBuckets
|
||||
func TestFSListBuckets(t *testing.T) {
|
||||
t.Skip()
|
||||
// Prepare for tests
|
||||
disk := filepath.Join(globalTestTmpDir, "minio-"+nextSuffix())
|
||||
defer os.RemoveAll(disk)
|
||||
|
||||
@@ -19,12 +19,10 @@ package cmd
|
||||
|
||||
import "github.com/minio/madmin-go"
|
||||
|
||||
// GatewayMinioSysTmp prefix is used in Azure gateway for save metadata sent by Initialize Multipart Upload API.
|
||||
// Gateway name backends
|
||||
const (
|
||||
GatewayMinioSysTmp = "minio.sys.tmp/"
|
||||
AzureBackendGateway = "azure"
|
||||
NASBackendGateway = "nas"
|
||||
S3BackendGateway = "s3"
|
||||
NASBackendGateway = "nas"
|
||||
S3BackendGateway = "s3"
|
||||
)
|
||||
|
||||
// Gateway represents a gateway backend.
|
||||
|
||||
@@ -19,6 +19,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/madmin-go"
|
||||
"github.com/minio/minio/internal/color"
|
||||
"github.com/minio/minio/internal/config"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/pkg/certs"
|
||||
@@ -292,6 +294,9 @@ func StartGateway(ctx *cli.Context, gw Gateway) {
|
||||
SecretKey: globalActiveCred.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, errFreshDisk) {
|
||||
err = config.ErrInvalidFSValue(err)
|
||||
}
|
||||
logger.FatalIf(err, "Unable to initialize gateway backend")
|
||||
}
|
||||
newObject = NewGatewayLayerWithLocker(newObject)
|
||||
|
||||
@@ -49,5 +49,5 @@ func TestPrintGatewayStartupMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
apiEndpoints := []string{"http://127.0.0.1:9000"}
|
||||
printGatewayStartupMessage(apiEndpoints, "azure")
|
||||
printGatewayStartupMessage(apiEndpoints, "s3")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,273 +0,0 @@
|
||||
/*
|
||||
* MinIO Object Storage (c) 2021 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 azure
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
minio "github.com/minio/minio/cmd"
|
||||
)
|
||||
|
||||
func TestParseStorageEndpoint(t *testing.T) {
|
||||
testCases := []struct {
|
||||
host string
|
||||
accountName string
|
||||
expectedURL string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
"", "myaccount", "https://myaccount.blob.core.windows.net", nil,
|
||||
},
|
||||
{
|
||||
"myaccount.blob.core.usgovcloudapi.net", "myaccount", "https://myaccount.blob.core.usgovcloudapi.net", nil,
|
||||
},
|
||||
{
|
||||
"http://localhost:10000", "myaccount", "http://localhost:10000/myaccount", nil,
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
endpointURL, err := parseStorageEndpoint(testCase.host, testCase.accountName)
|
||||
if err != testCase.expectedErr {
|
||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
if endpointURL.String() != testCase.expectedURL {
|
||||
t.Errorf("Test %d: Expected URL %s, got %s", i+1, testCase.expectedURL, endpointURL.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test canonical metadata.
|
||||
func TestS3MetaToAzureProperties(t *testing.T) {
|
||||
headers := map[string]string{
|
||||
"accept-encoding": "gzip",
|
||||
"content-encoding": "gzip",
|
||||
"cache-control": "age: 3600",
|
||||
"content-disposition": "dummy",
|
||||
"content-length": "10",
|
||||
"content-type": "application/javascript",
|
||||
"X-Amz-Meta-Hdr": "value",
|
||||
"X-Amz-Meta-X_test_key": "value",
|
||||
"X-Amz-Meta-X__test__key": "value",
|
||||
"X-Amz-Meta-X-Test__key": "value",
|
||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
// Only X-Amz-Meta- prefixed entries will be returned in
|
||||
// Metadata (without the prefix!)
|
||||
expectedHeaders := map[string]string{
|
||||
"Hdr": "value",
|
||||
"X__test__key": "value",
|
||||
"X____test____key": "value",
|
||||
"X_Test____key": "value",
|
||||
"X_Amz_Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"X_Amz_Matdesc": "{}",
|
||||
"X_Amz_Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
meta, _, err := s3MetaToAzureProperties(minio.GlobalContext, headers)
|
||||
if err != nil {
|
||||
t.Fatalf("Test failed, with %s", err)
|
||||
}
|
||||
if !reflect.DeepEqual(map[string]string(meta), expectedHeaders) {
|
||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedHeaders, meta)
|
||||
}
|
||||
headers = map[string]string{
|
||||
"invalid--meta": "value",
|
||||
}
|
||||
_, _, err = s3MetaToAzureProperties(minio.GlobalContext, headers)
|
||||
if err != nil {
|
||||
if _, ok := err.(minio.UnsupportedMetadata); !ok {
|
||||
t.Fatalf("Test failed with unexpected error %s, expected UnsupportedMetadata", err)
|
||||
}
|
||||
}
|
||||
|
||||
headers = map[string]string{
|
||||
"content-md5": "Dce7bmCX61zvxzP5QmfelQ==",
|
||||
}
|
||||
_, props, err := s3MetaToAzureProperties(minio.GlobalContext, headers)
|
||||
if err != nil {
|
||||
t.Fatalf("Test failed, with %s", err)
|
||||
}
|
||||
if base64.StdEncoding.EncodeToString(props.ContentMD5) != headers["content-md5"] {
|
||||
t.Fatalf("Test failed, expected %s, got %s", headers["content-md5"], props.ContentMD5)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzurePropertiesToS3Meta(t *testing.T) {
|
||||
// Just one testcase. Adding more test cases does not add value to the testcase
|
||||
// as azureToS3Metadata() just adds a prefix.
|
||||
metadata := map[string]string{
|
||||
"first_name": "myname",
|
||||
"x_test_key": "value",
|
||||
"x_test__key": "value",
|
||||
"x__test__key": "value",
|
||||
"x____test____key": "value",
|
||||
"x_amz_key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"x_amz_matdesc": "{}",
|
||||
"x_amz_iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
}
|
||||
expectedMeta := map[string]string{
|
||||
"X-Amz-Meta-First-Name": "myname",
|
||||
"X-Amz-Meta-X-Test-Key": "value",
|
||||
"X-Amz-Meta-X-Test_key": "value",
|
||||
"X-Amz-Meta-X_test_key": "value",
|
||||
"X-Amz-Meta-X__test__key": "value",
|
||||
"X-Amz-Meta-X-Amz-Key": "hu3ZSqtqwn+aL4V2VhAeov4i+bG3KyCtRMSXQFRHXOk=",
|
||||
"X-Amz-Meta-X-Amz-Matdesc": "{}",
|
||||
"X-Amz-Meta-X-Amz-Iv": "eWmyryl8kq+EVnnsE7jpOg==",
|
||||
"Cache-Control": "max-age: 3600",
|
||||
"Content-Disposition": "dummy",
|
||||
"Content-Encoding": "gzip",
|
||||
"Content-Length": "10",
|
||||
"Content-MD5": base64.StdEncoding.EncodeToString([]byte("base64-md5")),
|
||||
"Content-Type": "application/javascript",
|
||||
}
|
||||
actualMeta := azurePropertiesToS3Meta(metadata, azblob.BlobHTTPHeaders{
|
||||
CacheControl: "max-age: 3600",
|
||||
ContentDisposition: "dummy",
|
||||
ContentEncoding: "gzip",
|
||||
ContentMD5: []byte("base64-md5"),
|
||||
ContentType: "application/javascript",
|
||||
}, 10)
|
||||
if !reflect.DeepEqual(actualMeta, expectedMeta) {
|
||||
t.Fatalf("Test failed, expected %#v, got %#v", expectedMeta, actualMeta)
|
||||
}
|
||||
}
|
||||
|
||||
// Add tests for azure to object error (top level).
|
||||
func TestAzureToObjectError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
actualErr error
|
||||
expectedErr error
|
||||
bucket, object string
|
||||
}{
|
||||
{
|
||||
nil, nil, "", "",
|
||||
},
|
||||
{
|
||||
fmt.Errorf("Non azure error"),
|
||||
fmt.Errorf("Non azure error"), "", "",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
if err := azureToObjectError(testCase.actualErr, testCase.bucket, testCase.object); err != nil {
|
||||
if err.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
} else {
|
||||
if testCase.expectedErr != nil {
|
||||
t.Errorf("Test %d expected an error but one was not produced", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add tests for azure to object error (internal).
|
||||
func TestAzureCodesToObjectError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
originalErr error
|
||||
actualServiceCode string
|
||||
actualStatusCode int
|
||||
expectedErr error
|
||||
bucket, object string
|
||||
}{
|
||||
{
|
||||
nil, "ContainerAlreadyExists", 0,
|
||||
minio.BucketExists{Bucket: "bucket"},
|
||||
"bucket", "",
|
||||
},
|
||||
{
|
||||
nil, "InvalidResourceName", 0,
|
||||
minio.BucketNameInvalid{Bucket: "bucket."},
|
||||
"bucket.", "",
|
||||
},
|
||||
{
|
||||
nil, "RequestBodyTooLarge", 0,
|
||||
minio.PartTooBig{},
|
||||
"", "",
|
||||
},
|
||||
{
|
||||
nil, "InvalidMetadata", 0,
|
||||
minio.UnsupportedMetadata{},
|
||||
"", "",
|
||||
},
|
||||
{
|
||||
nil, "", http.StatusNotFound,
|
||||
minio.ObjectNotFound{
|
||||
Bucket: "bucket",
|
||||
Object: "object",
|
||||
},
|
||||
"bucket", "object",
|
||||
},
|
||||
{
|
||||
nil, "", http.StatusNotFound,
|
||||
minio.BucketNotFound{Bucket: "bucket"},
|
||||
"bucket", "",
|
||||
},
|
||||
{
|
||||
nil, "", http.StatusBadRequest,
|
||||
minio.BucketNameInvalid{Bucket: "bucket."},
|
||||
"bucket.", "",
|
||||
},
|
||||
{
|
||||
fmt.Errorf("unhandled azure error"), "", http.StatusForbidden,
|
||||
fmt.Errorf("unhandled azure error"), "", "",
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
if err := azureCodesToObjectError(testCase.originalErr, testCase.actualServiceCode, testCase.actualStatusCode, testCase.bucket, testCase.object); err != nil {
|
||||
if err.Error() != testCase.expectedErr.Error() {
|
||||
t.Errorf("Test %d: Expected error %s, got %s", i+1, testCase.expectedErr, err)
|
||||
}
|
||||
} else {
|
||||
if testCase.expectedErr != nil {
|
||||
t.Errorf("Test %d expected an error but one was not produced", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAzureUploadID(t *testing.T) {
|
||||
invalidUploadIDs := []string{
|
||||
"123456789abcdefg",
|
||||
"hello world",
|
||||
"0x1234567890",
|
||||
"1234567890abcdef1234567890abcdef",
|
||||
}
|
||||
|
||||
for _, uploadID := range invalidUploadIDs {
|
||||
if err := checkAzureUploadID(minio.GlobalContext, uploadID); err == nil {
|
||||
t.Fatalf("%s: expected: <error>, got: <nil>", uploadID)
|
||||
}
|
||||
}
|
||||
|
||||
validUploadIDs := []string{
|
||||
"1234567890abcdef",
|
||||
"1122334455667788",
|
||||
}
|
||||
|
||||
for _, uploadID := range validUploadIDs {
|
||||
if err := checkAzureUploadID(minio.GlobalContext, uploadID); err != nil {
|
||||
t.Fatalf("%s: expected: <nil>, got: %s", uploadID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,6 @@ import (
|
||||
// NAS
|
||||
_ "github.com/minio/minio/cmd/gateway/nas"
|
||||
|
||||
// Azure
|
||||
_ "github.com/minio/minio/cmd/gateway/azure"
|
||||
|
||||
// S3
|
||||
_ "github.com/minio/minio/cmd/gateway/s3"
|
||||
// gateway functionality is frozen, no new gateways are being implemented
|
||||
|
||||
@@ -364,6 +364,7 @@ func (l *s3EncObjects) GetObjectInfo(ctx context.Context, bucket string, object
|
||||
// CopyObject copies an object from source bucket to a destination bucket.
|
||||
func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
cpSrcDstSame := path.Join(srcBucket, srcObject) == path.Join(dstBucket, dstObject)
|
||||
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||
if cpSrcDstSame {
|
||||
var gwMeta gwMetaV1
|
||||
if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil &&
|
||||
@@ -378,16 +379,16 @@ func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObje
|
||||
d.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
userDefined[k] = v[0]
|
||||
}
|
||||
gwMeta.Meta = srcInfo.UserDefined
|
||||
gwMeta.Meta = userDefined
|
||||
if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err)
|
||||
}
|
||||
return gwMeta.ToObjectInfo(dstBucket, dstObject), nil
|
||||
}
|
||||
}
|
||||
dstOpts := minio.ObjectOptions{ServerSideEncryption: d.ServerSideEncryption, UserDefined: srcInfo.UserDefined}
|
||||
dstOpts := minio.ObjectOptions{ServerSideEncryption: d.ServerSideEncryption, UserDefined: userDefined}
|
||||
return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, dstOpts)
|
||||
}
|
||||
|
||||
|
||||
@@ -502,17 +502,20 @@ func (l *s3Objects) GetObjectInfo(ctx context.Context, bucket string, object str
|
||||
// PutObject creates a new object with the incoming data,
|
||||
func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string, r *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) {
|
||||
data := r.Reader
|
||||
|
||||
userDefined := minio.CloneMSS(opts.UserDefined)
|
||||
|
||||
var tagMap map[string]string
|
||||
if tagstr, ok := opts.UserDefined[xhttp.AmzObjectTagging]; ok && tagstr != "" {
|
||||
if tagstr, ok := userDefined[xhttp.AmzObjectTagging]; ok && tagstr != "" {
|
||||
tagObj, err := tags.ParseObjectTags(tagstr)
|
||||
if err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
tagMap = tagObj.ToMap()
|
||||
delete(opts.UserDefined, xhttp.AmzObjectTagging)
|
||||
delete(userDefined, xhttp.AmzObjectTagging)
|
||||
}
|
||||
putOpts := miniogo.PutObjectOptions{
|
||||
UserMetadata: opts.UserDefined,
|
||||
UserMetadata: userDefined,
|
||||
ServerSideEncryption: opts.ServerSideEncryption,
|
||||
UserTags: tagMap,
|
||||
// Content-Md5 is needed for buckets with object locking,
|
||||
@@ -529,7 +532,7 @@ func (l *s3Objects) PutObject(ctx context.Context, bucket string, object string,
|
||||
ETag: ui.ETag,
|
||||
Size: ui.Size,
|
||||
Key: object,
|
||||
Metadata: minio.ToMinioClientObjectInfoMetadata(opts.UserDefined),
|
||||
Metadata: minio.ToMinioClientObjectInfoMetadata(userDefined),
|
||||
}
|
||||
|
||||
return minio.FromMinioClientObjectInfo(bucket, oi), nil
|
||||
@@ -544,8 +547,9 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
||||
// metadata input is already a trickled down value from interpreting x-amz-metadata-directive at
|
||||
// handler layer. So what we have right now is supposed to be applied on the destination object anyways.
|
||||
// So preserve it by adding "REPLACE" directive to save all the metadata set by CopyObject API.
|
||||
srcInfo.UserDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||
srcInfo.UserDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||
userDefined["x-amz-metadata-directive"] = "REPLACE"
|
||||
userDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
@@ -556,10 +560,10 @@ func (l *s3Objects) CopyObject(ctx context.Context, srcBucket string, srcObject
|
||||
}
|
||||
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
userDefined[k] = v[0]
|
||||
}
|
||||
|
||||
if _, err = l.Client.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, srcInfo.UserDefined, miniogo.CopySrcOptions{}, miniogo.PutObjectOptions{}); err != nil {
|
||||
if _, err = l.Client.CopyObject(ctx, srcBucket, srcObject, dstBucket, dstObject, userDefined, miniogo.CopySrcOptions{}, miniogo.PutObjectOptions{}); err != nil {
|
||||
return objInfo, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
return l.GetObjectInfo(ctx, dstBucket, dstObject, dstOpts)
|
||||
@@ -605,17 +609,18 @@ func (l *s3Objects) ListMultipartUploads(ctx context.Context, bucket string, pre
|
||||
// NewMultipartUpload upload object in multiple parts
|
||||
func (l *s3Objects) NewMultipartUpload(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (uploadID string, err error) {
|
||||
var tagMap map[string]string
|
||||
if tagStr, ok := o.UserDefined[xhttp.AmzObjectTagging]; ok {
|
||||
userDefined := minio.CloneMSS(o.UserDefined)
|
||||
if tagStr, ok := userDefined[xhttp.AmzObjectTagging]; ok {
|
||||
tagObj, err := tags.Parse(tagStr, true)
|
||||
if err != nil {
|
||||
return uploadID, minio.ErrorRespToObjectError(err, bucket, object)
|
||||
}
|
||||
tagMap = tagObj.ToMap()
|
||||
delete(o.UserDefined, xhttp.AmzObjectTagging)
|
||||
delete(userDefined, xhttp.AmzObjectTagging)
|
||||
}
|
||||
// Create PutObject options
|
||||
opts := miniogo.PutObjectOptions{
|
||||
UserMetadata: o.UserDefined,
|
||||
UserMetadata: userDefined,
|
||||
ServerSideEncryption: o.ServerSideEncryption,
|
||||
UserTags: tagMap,
|
||||
}
|
||||
@@ -645,9 +650,8 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
||||
if srcOpts.CheckPrecondFn != nil && srcOpts.CheckPrecondFn(srcInfo) {
|
||||
return minio.PartInfo{}, minio.PreConditionFailed{}
|
||||
}
|
||||
srcInfo.UserDefined = map[string]string{
|
||||
"x-amz-copy-source-if-match": srcInfo.ETag,
|
||||
}
|
||||
userDefined := minio.CloneMSS(srcInfo.UserDefined)
|
||||
userDefined["x-amz-copy-source-if-match"] = srcInfo.ETag
|
||||
header := make(http.Header)
|
||||
if srcOpts.ServerSideEncryption != nil {
|
||||
encrypt.SSECopy(srcOpts.ServerSideEncryption).Marshal(header)
|
||||
@@ -657,11 +661,11 @@ func (l *s3Objects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, de
|
||||
dstOpts.ServerSideEncryption.Marshal(header)
|
||||
}
|
||||
for k, v := range header {
|
||||
srcInfo.UserDefined[k] = v[0]
|
||||
userDefined[k] = v[0]
|
||||
}
|
||||
|
||||
completePart, err := l.Client.CopyObjectPart(ctx, srcBucket, srcObject, destBucket, destObject,
|
||||
uploadID, partID, startOffset, length, srcInfo.UserDefined)
|
||||
uploadID, partID, startOffset, length, userDefined)
|
||||
if err != nil {
|
||||
return p, minio.ErrorRespToObjectError(err, srcBucket, srcObject)
|
||||
}
|
||||
|
||||
+53
-28
@@ -18,6 +18,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"path"
|
||||
@@ -141,6 +142,14 @@ func setBrowserRedirectHandler(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
var redirectPrefixes = map[string]struct{}{
|
||||
"favicon-16x16.png": {},
|
||||
"favicon-32x32.png": {},
|
||||
"favicon-96x96.png": {},
|
||||
"index.html": {},
|
||||
minioReservedBucket: {},
|
||||
}
|
||||
|
||||
// Fetch redirect location if urlPath satisfies certain
|
||||
// criteria. Some special names are considered to be
|
||||
// redirectable, this is purely internal function and
|
||||
@@ -151,32 +160,25 @@ func getRedirectLocation(r *http.Request) *xnet.URL {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, prefix := range []string{
|
||||
"favicon-16x16.png",
|
||||
"favicon-32x32.png",
|
||||
"favicon-96x96.png",
|
||||
"index.html",
|
||||
minioReservedBucket,
|
||||
} {
|
||||
bucket, _ := path2BucketObject(resource)
|
||||
if path.Clean(bucket) == prefix || resource == slashSeparator {
|
||||
if globalBrowserRedirectURL != nil {
|
||||
return globalBrowserRedirectURL
|
||||
}
|
||||
hostname, _, _ := net.SplitHostPort(r.Host)
|
||||
if hostname == "" {
|
||||
hostname = r.Host
|
||||
}
|
||||
return &xnet.URL{
|
||||
Host: net.JoinHostPort(hostname, globalMinioConsolePort),
|
||||
Scheme: func() string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme
|
||||
}(),
|
||||
}
|
||||
bucket, _ := path2BucketObject(resource)
|
||||
_, redirect := redirectPrefixes[path.Clean(bucket)]
|
||||
if redirect || resource == slashSeparator {
|
||||
if globalBrowserRedirectURL != nil {
|
||||
return globalBrowserRedirectURL
|
||||
}
|
||||
xhost, err := xnet.ParseHost(r.Host)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &xnet.URL{
|
||||
Host: net.JoinHostPort(xhost.Name, globalMinioConsolePort),
|
||||
Scheme: func() string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
return scheme
|
||||
}(),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -294,6 +296,15 @@ const (
|
||||
dotComponent = "."
|
||||
)
|
||||
|
||||
func hasBadHost(host string) error {
|
||||
if globalIsCICD && strings.TrimSpace(host) == "" {
|
||||
// under CI/CD test setups ignore empty hosts as invalid hosts
|
||||
return nil
|
||||
}
|
||||
_, err := xnet.ParseHost(host)
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the incoming path has bad path components,
|
||||
// such as ".." and "."
|
||||
func hasBadPathComponent(path string) bool {
|
||||
@@ -312,7 +323,11 @@ func hasBadPathComponent(path string) bool {
|
||||
// Check if client is sending a malicious request.
|
||||
func hasMultipleAuth(r *http.Request) bool {
|
||||
authTypeCount := 0
|
||||
for _, hasValidAuth := range []func(*http.Request) bool{isRequestSignatureV2, isRequestPresignedSignatureV2, isRequestSignatureV4, isRequestPresignedSignatureV4, isRequestJWT, isRequestPostPolicySignatureV4} {
|
||||
for _, hasValidAuth := range []func(*http.Request) bool{
|
||||
isRequestSignatureV2, isRequestPresignedSignatureV2,
|
||||
isRequestSignatureV4, isRequestPresignedSignatureV4,
|
||||
isRequestJWT, isRequestPostPolicySignatureV4,
|
||||
} {
|
||||
if hasValidAuth(r) {
|
||||
authTypeCount++
|
||||
}
|
||||
@@ -324,6 +339,14 @@ func hasMultipleAuth(r *http.Request) bool {
|
||||
// any malicious requests.
|
||||
func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := hasBadHost(r.Host); err != nil {
|
||||
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||
invalidReq.Description = fmt.Sprintf("%s (%s)", invalidReq.Description, err)
|
||||
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for bad components in URL path.
|
||||
if hasBadPathComponent(r.URL.Path) {
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidResourceName), r.URL)
|
||||
@@ -341,7 +364,9 @@ func setRequestValidityHandler(h http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
if hasMultipleAuth(r) {
|
||||
writeErrorResponse(r.Context(), w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
invalidReq := errorCodes.ToAPIErr(ErrInvalidRequest)
|
||||
invalidReq.Description = fmt.Sprintf("%s (request has multiple authentication types, please use one)", invalidReq.Description)
|
||||
writeErrorResponse(r.Context(), w, invalidReq, r.URL)
|
||||
atomic.AddUint64(&globalHTTPStats.rejectedRequestsInvalid, 1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -340,7 +340,9 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
||||
// healObject heals given object path in deep to fix bitrot.
|
||||
func healObject(bucket, object, versionID string, scan madmin.HealScanMode) {
|
||||
// Get background heal sequence to send elements to heal
|
||||
globalHealStateLK.Lock()
|
||||
bgSeq, ok := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||
globalHealStateLK.Unlock()
|
||||
if ok {
|
||||
bgSeq.queueHealTask(healSource{
|
||||
bucket: bucket,
|
||||
|
||||
+39
-6
@@ -37,12 +37,14 @@ import (
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config/cache"
|
||||
"github.com/minio/minio/internal/config/callhome"
|
||||
"github.com/minio/minio/internal/config/compress"
|
||||
"github.com/minio/minio/internal/config/dns"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
xtls "github.com/minio/minio/internal/config/identity/tls"
|
||||
"github.com/minio/minio/internal/config/policy/opa"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/config/subnet"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
@@ -73,6 +75,7 @@ const (
|
||||
globalWindowsOSName = "windows"
|
||||
globalMacOSName = "darwin"
|
||||
globalMinioModeFS = "mode-server-fs"
|
||||
globalMinioModeErasureSD = "mode-server-xl-single"
|
||||
globalMinioModeErasure = "mode-server-xl"
|
||||
globalMinioModeDistErasure = "mode-server-distributed-xl"
|
||||
globalMinioModeGatewayPrefix = "mode-gateway-"
|
||||
@@ -113,6 +116,10 @@ const (
|
||||
// diskFillFraction is the fraction of a disk we allow to be filled.
|
||||
diskFillFraction = 0.99
|
||||
|
||||
// diskReserveFraction is the fraction of a disk where we will fill other server pools first.
|
||||
// If all pools reach this, we will use all pools with regular placement.
|
||||
diskReserveFraction = 0.15
|
||||
|
||||
// diskAssumeUnknownSize is the size to assume when an unknown size upload is requested.
|
||||
diskAssumeUnknownSize = 1 << 30
|
||||
|
||||
@@ -136,10 +143,16 @@ var (
|
||||
// Indicates if the running minio server is an erasure-code backend.
|
||||
globalIsErasure = false
|
||||
|
||||
// Indicates if the running minio server is in single drive XL mode.
|
||||
globalIsErasureSD = false
|
||||
|
||||
// Indicates if the running minio is in gateway mode.
|
||||
globalIsGateway = false
|
||||
|
||||
// Name of gateway server, e.g S3, GCS, Azure, etc
|
||||
// Indicates if server code should go through testing path.
|
||||
globalIsTesting = false
|
||||
|
||||
// Name of gateway server, e.g S3, NAS etc
|
||||
globalGatewayName = ""
|
||||
|
||||
// This flag is set to 'true' by default
|
||||
@@ -193,10 +206,13 @@ var (
|
||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||
|
||||
globalStorageClass storageclass.Config
|
||||
|
||||
globalLDAPConfig xldap.Config
|
||||
globalOpenIDConfig openid.Config
|
||||
globalSTSTLSConfig xtls.Config
|
||||
|
||||
globalAuthNPlugin *idplugin.AuthNPlugin
|
||||
|
||||
// CA root certificates, a nil value means system certs pool will be used
|
||||
globalRootCAs *x509.CertPool
|
||||
|
||||
@@ -211,10 +227,10 @@ var (
|
||||
|
||||
// global Trace system to send HTTP request/response
|
||||
// and Storage/OS calls info to registered listeners.
|
||||
globalTrace = pubsub.New()
|
||||
globalTrace = pubsub.New(8)
|
||||
|
||||
// global Listen system to send S3 API events to registered listeners
|
||||
globalHTTPListen = pubsub.New()
|
||||
globalHTTPListen = pubsub.New(0)
|
||||
|
||||
// global console system to send console logs to
|
||||
// registered listeners
|
||||
@@ -228,6 +244,9 @@ var (
|
||||
// The global subnet config
|
||||
globalSubnetConfig subnet.Config
|
||||
|
||||
// The global callhome config
|
||||
globalCallhomeConfig callhome.Config
|
||||
|
||||
globalRemoteEndpoints map[string]Endpoint
|
||||
|
||||
// Global server's network statistics
|
||||
@@ -290,8 +309,8 @@ var (
|
||||
// Some standard content-types which we strictly dis-allow for compression.
|
||||
standardExcludeCompressContentTypes = []string{"video/*", "audio/*", "application/zip", "application/x-gzip", "application/x-zip-compressed", " application/x-compress", "application/x-spoon"}
|
||||
|
||||
// OPA policy system.
|
||||
globalPolicyOPA *opa.Opa
|
||||
// AuthZ Plugin system.
|
||||
globalAuthZPlugin *polplugin.AuthZPlugin
|
||||
|
||||
// Deployment ID - unique per deployment
|
||||
globalDeploymentID string
|
||||
@@ -352,4 +371,18 @@ var (
|
||||
// Add new variable global values here.
|
||||
)
|
||||
|
||||
var globalAuthZPluginMutex sync.Mutex
|
||||
|
||||
func newGlobalAuthZPluginFn() *polplugin.AuthZPlugin {
|
||||
globalAuthZPluginMutex.Lock()
|
||||
defer globalAuthZPluginMutex.Unlock()
|
||||
return globalAuthZPlugin
|
||||
}
|
||||
|
||||
func setGlobalAuthZPlugin(authz *polplugin.AuthZPlugin) {
|
||||
globalAuthZPluginMutex.Lock()
|
||||
globalAuthZPlugin = authz
|
||||
globalAuthZPluginMutex.Unlock()
|
||||
}
|
||||
|
||||
var errSelfTestFailure = errors.New("self test failed. unsafe to start server")
|
||||
|
||||
+1
-4
@@ -116,10 +116,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||
// + 2 * 10MiB (default erasure block size v1) + 2 * 1MiB (default erasure block size v2)
|
||||
blockSize := xioutil.BlockSizeLarge + xioutil.BlockSizeSmall
|
||||
apiRequestsMaxPerNode = int(maxMem / uint64(maxSetDrives*blockSize+int(blockSizeV1*2+blockSizeV2*2)))
|
||||
|
||||
if globalIsErasure {
|
||||
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
|
||||
}
|
||||
logger.Info("Automatically configured API requests per node based on available memory on the system: %d", apiRequestsMaxPerNode)
|
||||
} else {
|
||||
apiRequestsMaxPerNode = cfg.RequestsMax
|
||||
if len(globalEndpoints.Hostnames()) > 0 {
|
||||
|
||||
+17
-14
@@ -25,7 +25,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
@@ -37,6 +36,7 @@ import (
|
||||
"github.com/minio/minio/internal/handlers"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
xnet "github.com/minio/pkg/net"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -237,6 +237,10 @@ func extractReqParams(r *http.Request) map[string]string {
|
||||
"sourceIPAddress": handlers.GetSourceIP(r),
|
||||
// Add more fields here.
|
||||
}
|
||||
if rangeField := r.Header.Get(xhttp.Range); rangeField != "" {
|
||||
m["range"] = rangeField
|
||||
}
|
||||
|
||||
if _, ok := r.Header[xhttp.MinIOSourceReplicationRequest]; ok {
|
||||
m[xhttp.MinIOSourceReplicationRequest] = ""
|
||||
}
|
||||
@@ -395,26 +399,25 @@ func getResource(path string, host string, domains []string) (string, error) {
|
||||
if len(domains) == 0 {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// If virtual-host-style is enabled construct the "resource" properly.
|
||||
if strings.Contains(host, ":") {
|
||||
// In bucket.mydomain.com:9000, strip out :9000
|
||||
var err error
|
||||
if host, _, err = net.SplitHostPort(host); err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("host", host)
|
||||
reqInfo.AppendTags("path", path)
|
||||
ctx := logger.SetReqInfo(GlobalContext, reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
xhost, err := xnet.ParseHost(host)
|
||||
if err != nil {
|
||||
reqInfo := (&logger.ReqInfo{}).AppendTags("host", host)
|
||||
reqInfo.AppendTags("path", path)
|
||||
ctx := logger.SetReqInfo(context.Background(), reqInfo)
|
||||
logger.LogIf(ctx, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
if host == minioReservedBucket+"."+domain {
|
||||
if xhost.Name == minioReservedBucket+"."+domain {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(host, "."+domain) {
|
||||
if !strings.HasSuffix(xhost.Name, "."+domain) {
|
||||
continue
|
||||
}
|
||||
bucket := strings.TrimSuffix(host, "."+domain)
|
||||
bucket := strings.TrimSuffix(xhost.Name, "."+domain)
|
||||
return SlashSeparator + pathJoin(bucket, path), nil
|
||||
}
|
||||
return path, nil
|
||||
|
||||
@@ -221,6 +221,9 @@ func TestGetResource(t *testing.T) {
|
||||
expectedResource string
|
||||
}{
|
||||
{"/a/b/c", "test.mydomain.com", []string{"mydomain.com"}, "/test/a/b/c"},
|
||||
{"/a/b/c", "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", []string{"mydomain.com"}, "/a/b/c"},
|
||||
{"/a/b/c", "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", []string{"mydomain.com"}, "/a/b/c"},
|
||||
{"/a/b/c", "192.168.1.1:9000", []string{"mydomain.com"}, "/a/b/c"},
|
||||
{"/a/b/c", "test.mydomain.com", []string{"notmydomain.com"}, "/a/b/c"},
|
||||
{"/a/b/c", "test.mydomain.com", nil, "/a/b/c"},
|
||||
}
|
||||
|
||||
+2
-1
@@ -19,8 +19,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
)
|
||||
|
||||
// getSHA256Hash returns SHA-256 hash in hex encoding of given data.
|
||||
|
||||
+131
-22
@@ -158,8 +158,7 @@ type MappedPolicy struct {
|
||||
func (mp MappedPolicy) toSlice() []string {
|
||||
var policies []string
|
||||
for _, policy := range strings.Split(mp.Policies, ",") {
|
||||
policy = strings.TrimSpace(policy)
|
||||
if policy == "" {
|
||||
if strings.TrimSpace(policy) == "" {
|
||||
continue
|
||||
}
|
||||
policies = append(policies, policy)
|
||||
@@ -245,6 +244,8 @@ type iamWatchEvent struct {
|
||||
|
||||
// iamCache contains in-memory cache of IAM data.
|
||||
type iamCache struct {
|
||||
updatedAt time.Time
|
||||
|
||||
// map of policy names to policy definitions
|
||||
iamPolicyDocsMap map[string]PolicyDoc
|
||||
// map of usernames to credentials
|
||||
@@ -433,8 +434,7 @@ func setDefaultCannedPolicies(policies map[string]PolicyDoc) {
|
||||
func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
|
||||
newCache := newIamCache()
|
||||
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
loadedAt := time.Now()
|
||||
|
||||
if iamOS, ok := store.IAMStorageAPI.(*IAMObjectStore); ok {
|
||||
err := iamOS.loadAllFromObjStore(ctx, newCache)
|
||||
@@ -487,12 +487,28 @@ func (store *IAMStoreSys) LoadIAMCache(ctx context.Context) error {
|
||||
newCache.buildUserGroupMemberships()
|
||||
}
|
||||
|
||||
cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap
|
||||
cache.iamGroupsMap = newCache.iamGroupsMap
|
||||
cache.iamPolicyDocsMap = newCache.iamPolicyDocsMap
|
||||
cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships
|
||||
cache.iamUserPolicyMap = newCache.iamUserPolicyMap
|
||||
cache.iamUsersMap = newCache.iamUsersMap
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
// We should only update the in-memory cache if there were no changes
|
||||
// to the in-memory cache since the disk loading began. If there
|
||||
// were changes to the in-memory cache we should wait for the next
|
||||
// cycle until we can safely update the in-memory cache.
|
||||
//
|
||||
// An in-memory cache must be replaced only if we know for sure that
|
||||
// the values loaded from disk are not stale. They might be stale
|
||||
// if the cached.updatedAt is recent than the refresh cycle began.
|
||||
if cache.updatedAt.Before(loadedAt) {
|
||||
// No one has updated anything since the config was loaded,
|
||||
// so we just replace whatever is on the disk into memory.
|
||||
cache.iamGroupPolicyMap = newCache.iamGroupPolicyMap
|
||||
cache.iamGroupsMap = newCache.iamGroupsMap
|
||||
cache.iamPolicyDocsMap = newCache.iamPolicyDocsMap
|
||||
cache.iamUserGroupMemberships = newCache.iamUserGroupMemberships
|
||||
cache.iamUserPolicyMap = newCache.iamUserPolicyMap
|
||||
cache.iamUsersMap = newCache.iamUsersMap
|
||||
cache.updatedAt = time.Now()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -549,6 +565,8 @@ func (store *IAMStoreSys) GroupNotificationHandler(ctx context.Context, group st
|
||||
cache.removeGroupFromMembershipsMap(group)
|
||||
delete(cache.iamGroupsMap, group)
|
||||
delete(cache.iamGroupPolicyMap, group)
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -563,6 +581,7 @@ func (store *IAMStoreSys) GroupNotificationHandler(ctx context.Context, group st
|
||||
// removed, the cache stays current.
|
||||
cache.removeGroupFromMembershipsMap(group)
|
||||
cache.updateGroupMembershipsMap(group, &gi)
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -640,6 +659,8 @@ func (store *IAMStoreSys) AddUsersToGroup(ctx context.Context, group string, mem
|
||||
cache.iamUserGroupMemberships[member] = gset
|
||||
}
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -673,6 +694,7 @@ func removeMembersFromGroup(ctx context.Context, store *IAMStoreSys, cache *iamC
|
||||
cache.iamUserGroupMemberships[member] = gset
|
||||
}
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -721,6 +743,7 @@ func (store *IAMStoreSys) RemoveUsersFromGroup(ctx context.Context, group string
|
||||
// Delete from server memory
|
||||
delete(cache.iamGroupsMap, group)
|
||||
delete(cache.iamGroupPolicyMap, group)
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -750,7 +773,10 @@ func (store *IAMStoreSys) SetGroupStatus(ctx context.Context, group string, enab
|
||||
if err := store.saveGroupInfo(ctx, group, gi); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.iamGroupsMap[group] = gi
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -801,7 +827,7 @@ func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err err
|
||||
return
|
||||
}
|
||||
cache.iamGroupsMap = m
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
for k := range cache.iamGroupsMap {
|
||||
res = append(res, k)
|
||||
}
|
||||
@@ -814,6 +840,7 @@ func (store *IAMStoreSys) ListGroups(ctx context.Context) (res []string, err err
|
||||
return
|
||||
}
|
||||
cache.iamGroupPolicyMap = m
|
||||
cache.updatedAt = time.Now()
|
||||
for k := range cache.iamGroupPolicyMap {
|
||||
res = append(res, k)
|
||||
}
|
||||
@@ -862,6 +889,8 @@ func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string,
|
||||
} else {
|
||||
delete(cache.iamGroupPolicyMap, name)
|
||||
}
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -882,6 +911,7 @@ func (store *IAMStoreSys) PolicyDBSet(ctx context.Context, name, policy string,
|
||||
} else {
|
||||
cache.iamGroupPolicyMap[name] = mp
|
||||
}
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -931,6 +961,7 @@ func (store *IAMStoreSys) PolicyNotificationHandler(ctx context.Context, policy
|
||||
cache.iamGroupPolicyMap[g] = newMappedPolicy(strings.Join(pset.ToSlice(), ","))
|
||||
}
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -988,6 +1019,8 @@ func (store *IAMStoreSys) DeletePolicy(ctx context.Context, policy string) error
|
||||
}
|
||||
|
||||
delete(cache.iamPolicyDocsMap, policy)
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1058,6 +1091,8 @@ func (store *IAMStoreSys) SetPolicy(ctx context.Context, name string, policy iam
|
||||
}
|
||||
|
||||
cache.iamPolicyDocsMap[name] = d
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1077,6 +1112,7 @@ func (store *IAMStoreSys) ListPolicies(ctx context.Context, bucketName string) (
|
||||
setDefaultCannedPolicies(m)
|
||||
|
||||
cache.iamPolicyDocsMap = m
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
ret := map[string]iampolicy.Policy{}
|
||||
for k, v := range m {
|
||||
@@ -1104,6 +1140,7 @@ func (store *IAMStoreSys) ListPolicyDocs(ctx context.Context, bucketName string)
|
||||
setDefaultCannedPolicies(m)
|
||||
|
||||
cache.iamPolicyDocsMap = m
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
ret := map[string]PolicyDoc{}
|
||||
for k, v := range m {
|
||||
@@ -1300,6 +1337,8 @@ func (store *IAMStoreSys) PolicyMappingNotificationHandler(ctx context.Context,
|
||||
// This means that the policy mapping was deleted, so we update
|
||||
// the cache.
|
||||
delete(m, userOrGroup)
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
@@ -1348,11 +1387,15 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
|
||||
|
||||
// 3. Delete any mapped policy
|
||||
delete(cache.iamUserPolicyMap, accessKey)
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if userType != svcUser {
|
||||
err = store.loadMappedPolicy(ctx, accessKey, userType, false, cache.iamUserPolicyMap)
|
||||
// Ignore policy not mapped error
|
||||
@@ -1374,6 +1417,7 @@ func (store *IAMStoreSys) UserNotificationHandler(ctx context.Context, accessKey
|
||||
if cred.IsTemp() && cred.ParentUser != "" && cred.ParentUser != globalActiveCred.AccessKey {
|
||||
if _, ok := cache.iamUserPolicyMap[cred.ParentUser]; !ok {
|
||||
cache.iamUserPolicyMap[cred.ParentUser] = cache.iamUserPolicyMap[accessKey]
|
||||
cache.updatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1431,6 +1475,8 @@ func (store *IAMStoreSys) DeleteUser(ctx context.Context, accessKey string, user
|
||||
}
|
||||
delete(cache.iamUsersMap, accessKey)
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1470,6 +1516,9 @@ func (store *IAMStoreSys) SetTempUser(ctx context.Context, accessKey string, cre
|
||||
}
|
||||
|
||||
cache.iamUsersMap[accessKey] = cred
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1480,6 +1529,7 @@ func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
var deleted bool
|
||||
usersToDelete := set.CreateStringSet(users...)
|
||||
for user, cred := range cache.iamUsersMap {
|
||||
userType := regUser
|
||||
@@ -1498,9 +1548,15 @@ func (store *IAMStoreSys) DeleteUsers(ctx context.Context, users []string) error
|
||||
err := store.deleteUserIdentity(ctx, user, userType)
|
||||
logger.LogIf(GlobalContext, err)
|
||||
delete(cache.iamUsersMap, user)
|
||||
|
||||
deleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if deleted {
|
||||
cache.updatedAt = time.Now()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1611,6 +1667,8 @@ func (store *IAMStoreSys) SetUserStatus(ctx context.Context, accessKey string, s
|
||||
}
|
||||
|
||||
cache.iamUsersMap[accessKey] = uinfo.Credentials
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1644,6 +1702,7 @@ func (store *IAMStoreSys) AddServiceAccount(ctx context.Context, cred auth.Crede
|
||||
}
|
||||
|
||||
cache.iamUsersMap[u.Credentials.AccessKey] = u.Credentials
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1685,6 +1744,17 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
return fmt.Errorf("unable to get svc acc claims: %v", err)
|
||||
}
|
||||
|
||||
// Extracted session policy name string can be removed as its not useful
|
||||
// at this point.
|
||||
delete(m, sessionPolicyNameExtracted)
|
||||
|
||||
// sessionPolicy is nil and there is embedded policy attached we remove
|
||||
// rembedded policy at that point.
|
||||
if _, ok := m[iampolicy.SessionPolicyName]; ok && opts.sessionPolicy == nil {
|
||||
delete(m, iampolicy.SessionPolicyName)
|
||||
m[iamPolicyClaimNameSA()] = inheritedPolicyType
|
||||
}
|
||||
|
||||
if opts.sessionPolicy != nil {
|
||||
if err := opts.sessionPolicy.Validate(); err != nil {
|
||||
return err
|
||||
@@ -1701,7 +1771,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
|
||||
// Overwrite session policy claims.
|
||||
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
|
||||
m[iamPolicyClaimNameSA()] = "embedded-policy"
|
||||
m[iamPolicyClaimNameSA()] = embeddedPolicyType
|
||||
}
|
||||
|
||||
cr.SessionToken, err = auth.JWTSignWithAccessKey(accessKey, m, cr.SecretKey)
|
||||
@@ -1715,6 +1785,7 @@ func (store *IAMStoreSys) UpdateServiceAccount(ctx context.Context, accessKey st
|
||||
}
|
||||
|
||||
cache.iamUsersMap[u.Credentials.AccessKey] = u.Credentials
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1724,14 +1795,29 @@ func (store *IAMStoreSys) ListTempAccounts(ctx context.Context, accessKey string
|
||||
cache := store.rlock()
|
||||
defer store.runlock()
|
||||
|
||||
userExists := false
|
||||
var tempAccounts []auth.Credentials
|
||||
for _, v := range cache.iamUsersMap {
|
||||
if v.IsTemp() && v.ParentUser == accessKey {
|
||||
// Hide secret key & session key here
|
||||
v.SecretKey = ""
|
||||
v.SessionToken = ""
|
||||
tempAccounts = append(tempAccounts, v)
|
||||
isDerived := false
|
||||
if v.IsServiceAccount() || v.IsTemp() {
|
||||
isDerived = true
|
||||
}
|
||||
|
||||
if !isDerived && v.AccessKey == accessKey {
|
||||
userExists = true
|
||||
} else if isDerived && v.ParentUser == accessKey {
|
||||
userExists = true
|
||||
if v.IsTemp() {
|
||||
// Hide secret key & session key here
|
||||
v.SecretKey = ""
|
||||
v.SessionToken = ""
|
||||
tempAccounts = append(tempAccounts, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !userExists {
|
||||
return nil, errNoSuchUser
|
||||
}
|
||||
|
||||
return tempAccounts, nil
|
||||
@@ -1742,14 +1828,29 @@ func (store *IAMStoreSys) ListServiceAccounts(ctx context.Context, accessKey str
|
||||
cache := store.rlock()
|
||||
defer store.runlock()
|
||||
|
||||
userExists := false
|
||||
var serviceAccounts []auth.Credentials
|
||||
for _, v := range cache.iamUsersMap {
|
||||
if v.IsServiceAccount() && v.ParentUser == accessKey {
|
||||
// Hide secret key & session key here
|
||||
v.SecretKey = ""
|
||||
v.SessionToken = ""
|
||||
serviceAccounts = append(serviceAccounts, v)
|
||||
isDerived := false
|
||||
if v.IsServiceAccount() || v.IsTemp() {
|
||||
isDerived = true
|
||||
}
|
||||
|
||||
if !isDerived && v.AccessKey == accessKey {
|
||||
userExists = true
|
||||
} else if isDerived && v.ParentUser == accessKey {
|
||||
userExists = true
|
||||
if v.IsServiceAccount() {
|
||||
// Hide secret key & session key here
|
||||
v.SecretKey = ""
|
||||
v.SessionToken = ""
|
||||
serviceAccounts = append(serviceAccounts, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !userExists {
|
||||
return nil, errNoSuchUser
|
||||
}
|
||||
|
||||
return serviceAccounts, nil
|
||||
@@ -1760,6 +1861,8 @@ func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq ma
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
cr, ok := cache.iamUsersMap[accessKey]
|
||||
|
||||
// It is not possible to update an STS account.
|
||||
@@ -1793,6 +1896,8 @@ func (store *IAMStoreSys) UpdateUserSecretKey(ctx context.Context, accessKey, se
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
cred, ok := cache.iamUsersMap[accessKey]
|
||||
if !ok {
|
||||
return errNoSuchUser
|
||||
@@ -1827,6 +1932,8 @@ func (store *IAMStoreSys) UpdateUserIdentity(ctx context.Context, cred auth.Cred
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
userType := regUser
|
||||
if cred.IsServiceAccount() {
|
||||
userType = svcUser
|
||||
@@ -1848,6 +1955,8 @@ func (store *IAMStoreSys) LoadUser(ctx context.Context, accessKey string) {
|
||||
cache := store.lock()
|
||||
defer store.unlock()
|
||||
|
||||
cache.updatedAt = time.Now()
|
||||
|
||||
_, found := cache.iamUsersMap[accessKey]
|
||||
if !found {
|
||||
store.loadUser(ctx, accessKey, regUser, cache.iamUsersMap)
|
||||
|
||||
+91
-85
@@ -38,6 +38,8 @@ import (
|
||||
"github.com/minio/minio/internal/arn"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/color"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
"github.com/minio/minio/internal/jwt"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
@@ -63,6 +65,11 @@ const (
|
||||
statusDisabled = "disabled"
|
||||
)
|
||||
|
||||
const (
|
||||
embeddedPolicyType = "embedded-policy"
|
||||
inheritedPolicyType = "inherited-policy"
|
||||
)
|
||||
|
||||
// IAMSys - config system.
|
||||
type IAMSys struct {
|
||||
// Need to keep them here to keep alignment - ref: https://golang.org/pkg/sync/atomic/#pkg-note-BUG
|
||||
@@ -75,6 +82,8 @@ type IAMSys struct {
|
||||
sync.Mutex
|
||||
|
||||
iamRefreshInterval time.Duration
|
||||
ldapConfig xldap.Config // only valid if usersSysType is LDAPUsers
|
||||
openIDConfig openid.Config // only valid if OpenID is configured
|
||||
|
||||
usersSysType UsersSysType
|
||||
|
||||
@@ -159,7 +168,7 @@ func (sys *IAMSys) doIAMConfigMigration(ctx context.Context) error {
|
||||
|
||||
// initStore initializes IAM stores
|
||||
func (sys *IAMSys) initStore(objAPI ObjectLayer, etcdClient *etcd.Client) {
|
||||
if globalLDAPConfig.Enabled {
|
||||
if sys.ldapConfig.Enabled {
|
||||
sys.EnableLDAPSys()
|
||||
}
|
||||
|
||||
@@ -219,6 +228,8 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
sys.Lock()
|
||||
defer sys.Unlock()
|
||||
|
||||
sys.ldapConfig = globalLDAPConfig.Clone()
|
||||
sys.openIDConfig = globalOpenIDConfig.Clone()
|
||||
sys.iamRefreshInterval = iamRefreshInterval
|
||||
|
||||
// Initialize IAM store
|
||||
@@ -303,30 +314,37 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
break
|
||||
}
|
||||
|
||||
refreshInterval := sys.iamRefreshInterval
|
||||
|
||||
// Set up polling for expired accounts and credentials purging.
|
||||
switch {
|
||||
case globalOpenIDConfig.ProviderEnabled():
|
||||
case sys.openIDConfig.ProviderEnabled():
|
||||
go func() {
|
||||
ticker := time.NewTicker(sys.iamRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
timer := time.NewTimer(refreshInterval)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-timer.C:
|
||||
sys.purgeExpiredCredentialsForExternalSSO(ctx)
|
||||
|
||||
timer.Reset(refreshInterval)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
case globalLDAPConfig.Enabled:
|
||||
case sys.ldapConfig.Enabled:
|
||||
go func() {
|
||||
ticker := time.NewTicker(sys.iamRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
timer := time.NewTimer(refreshInterval)
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-timer.C:
|
||||
sys.purgeExpiredCredentialsForLDAP(ctx)
|
||||
sys.updateGroupMembershipsForLDAP(ctx)
|
||||
|
||||
timer.Reset(refreshInterval)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -337,20 +355,18 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
// Start watching changes to storage.
|
||||
go sys.watch(ctx)
|
||||
|
||||
// Load RoleARN
|
||||
if rolePolicyMap := globalOpenIDConfig.GetRoleInfo(); rolePolicyMap != nil {
|
||||
// Validate that policies associated with roles are defined.
|
||||
for _, rolePolicies := range rolePolicyMap {
|
||||
ps := newMappedPolicy(rolePolicies).toSlice()
|
||||
numPolicies := len(ps)
|
||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
||||
numValidPolicies := len(strings.Split(validPolicies, ","))
|
||||
if numPolicies != numValidPolicies {
|
||||
logger.LogIf(ctx, fmt.Errorf("Some specified role policies (in %s) were not defined - role policies will not be enabled.", rolePolicies))
|
||||
return
|
||||
}
|
||||
}
|
||||
sys.rolesMap = rolePolicyMap
|
||||
// Load RoleARNs
|
||||
sys.rolesMap = make(map[arn.ARN]string)
|
||||
|
||||
// From OpenID
|
||||
if riMap := globalOpenIDConfig.GetRoleInfo(); riMap != nil {
|
||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||
}
|
||||
|
||||
// From AuthN plugin if enabled.
|
||||
if globalAuthNPlugin != nil {
|
||||
riMap := globalAuthNPlugin.GetRoleInfo()
|
||||
sys.validateAndAddRolePolicyMappings(ctx, riMap)
|
||||
}
|
||||
|
||||
sys.printIAMRoles()
|
||||
@@ -359,6 +375,32 @@ func (sys *IAMSys) Init(ctx context.Context, objAPI ObjectLayer, etcdClient *etc
|
||||
logger.Info("Finished loading IAM sub-system (took %.1fs of %.1fs to load data).", now.Sub(iamLoadStart).Seconds(), now.Sub(iamInitStart).Seconds())
|
||||
}
|
||||
|
||||
func (sys *IAMSys) validateAndAddRolePolicyMappings(ctx context.Context, m map[arn.ARN]string) {
|
||||
// Validate that policies associated with roles are defined. If
|
||||
// authZ plugin is set, role policies are just claims sent to
|
||||
// the plugin and they need not exist.
|
||||
//
|
||||
// If some mapped policies do not exist, we print some error
|
||||
// messages but continue any way - they can be fixed in the
|
||||
// running server by creating the policies after start up.
|
||||
for arn, rolePolicies := range m {
|
||||
specifiedPoliciesSet := newMappedPolicy(rolePolicies).policySet()
|
||||
validPolicies, _ := sys.store.FilterPolicies(rolePolicies, "")
|
||||
knownPoliciesSet := newMappedPolicy(validPolicies).policySet()
|
||||
unknownPoliciesSet := specifiedPoliciesSet.Difference(knownPoliciesSet)
|
||||
if len(unknownPoliciesSet) > 0 {
|
||||
if globalAuthZPlugin == nil {
|
||||
// Print a warning that some policies mapped to a role are not defined.
|
||||
errMsg := fmt.Errorf(
|
||||
"The policies \"%s\" mapped to role ARN %s are not defined - this role may not work as expected.",
|
||||
unknownPoliciesSet.ToSlice(), arn.String())
|
||||
logger.LogIf(ctx, errMsg)
|
||||
}
|
||||
}
|
||||
sys.rolesMap[arn] = rolePolicies
|
||||
}
|
||||
}
|
||||
|
||||
// Prints IAM role ARNs.
|
||||
func (sys *IAMSys) printIAMRoles() {
|
||||
if len(sys.rolesMap) == 0 {
|
||||
@@ -398,12 +440,12 @@ func (sys *IAMSys) watch(ctx context.Context) {
|
||||
|
||||
var maxRefreshDurationSecondsForLog float64 = 10
|
||||
|
||||
// Fall back to loading all items periodically
|
||||
ticker := time.NewTicker(sys.iamRefreshInterval)
|
||||
defer ticker.Stop()
|
||||
// Load all items periodically
|
||||
timer := time.NewTimer(sys.iamRefreshInterval)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-timer.C:
|
||||
refreshStart := time.Now()
|
||||
if err := sys.Load(ctx); err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Failure in periodic refresh for IAM (took %.2fs): %v", time.Since(refreshStart).Seconds(), err))
|
||||
@@ -415,6 +457,7 @@ func (sys *IAMSys) watch(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
timer.Reset(sys.iamRefreshInterval)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
@@ -676,7 +719,7 @@ func (sys *IAMSys) SetTempUser(ctx context.Context, accessKey string, cred auth.
|
||||
return errServerNotInitialized
|
||||
}
|
||||
|
||||
if globalPolicyOPA != nil {
|
||||
if newGlobalAuthZPluginFn() != nil {
|
||||
// If OPA is set, we do not need to set a policy mapping.
|
||||
policyName = ""
|
||||
}
|
||||
@@ -866,9 +909,9 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||
|
||||
if len(policyBuf) > 0 {
|
||||
m[iampolicy.SessionPolicyName] = base64.StdEncoding.EncodeToString(policyBuf)
|
||||
m[iamPolicyClaimNameSA()] = "embedded-policy"
|
||||
m[iamPolicyClaimNameSA()] = embeddedPolicyType
|
||||
} else {
|
||||
m[iamPolicyClaimNameSA()] = "inherited-policy"
|
||||
m[iamPolicyClaimNameSA()] = inheritedPolicyType
|
||||
}
|
||||
|
||||
// Add all the necessary claims for the service accounts.
|
||||
@@ -989,7 +1032,7 @@ func (sys *IAMSys) getServiceAccount(ctx context.Context, accessKey string) (aut
|
||||
}
|
||||
pt, ptok := jwtClaims.Lookup(iamPolicyClaimNameSA())
|
||||
sp, spok := jwtClaims.Lookup(iampolicy.SessionPolicyName)
|
||||
if ptok && spok && pt == "embedded-policy" {
|
||||
if ptok && spok && pt == embeddedPolicyType {
|
||||
policyBytes, err := base64.StdEncoding.DecodeString(sp)
|
||||
if err != nil {
|
||||
return auth.Credentials{}, nil, err
|
||||
@@ -1127,7 +1170,7 @@ func (sys *IAMSys) purgeExpiredCredentialsForExternalSSO(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
roleArn = roleArns[0]
|
||||
u, err := globalOpenIDConfig.LookupUser(roleArn, puInfo.subClaimValue)
|
||||
u, err := sys.openIDConfig.LookupUser(roleArn, puInfo.subClaimValue)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
continue
|
||||
@@ -1149,14 +1192,14 @@ func (sys *IAMSys) purgeExpiredCredentialsForLDAP(ctx context.Context) {
|
||||
parentUsers := sys.store.GetAllParentUsers()
|
||||
var allDistNames []string
|
||||
for parentUser := range parentUsers {
|
||||
if !globalLDAPConfig.IsLDAPUserDN(parentUser) {
|
||||
if !sys.ldapConfig.IsLDAPUserDN(parentUser) {
|
||||
continue
|
||||
}
|
||||
|
||||
allDistNames = append(allDistNames, parentUser)
|
||||
}
|
||||
|
||||
expiredUsers, err := globalLDAPConfig.GetNonEligibleUserDistNames(allDistNames)
|
||||
expiredUsers, err := sys.ldapConfig.GetNonEligibleUserDistNames(allDistNames)
|
||||
if err != nil {
|
||||
// Log and return on error - perhaps it'll work the next time.
|
||||
logger.LogIf(GlobalContext, err)
|
||||
@@ -1178,7 +1221,7 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
||||
// DN to ldap username mapping for each LDAP user
|
||||
parentUserToLDAPUsernameMap := make(map[string]string)
|
||||
for _, cred := range allCreds {
|
||||
if !globalLDAPConfig.IsLDAPUserDN(cred.ParentUser) {
|
||||
if !sys.ldapConfig.IsLDAPUserDN(cred.ParentUser) {
|
||||
continue
|
||||
}
|
||||
// Check if this is the first time we are
|
||||
@@ -1226,7 +1269,7 @@ func (sys *IAMSys) updateGroupMembershipsForLDAP(ctx context.Context) {
|
||||
}
|
||||
|
||||
// 2. Query LDAP server for groups of the LDAP users collected.
|
||||
updatedGroups, err := globalLDAPConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
|
||||
updatedGroups, err := sys.ldapConfig.LookupGroupMemberships(parentUsers, parentUserToLDAPUsernameMap)
|
||||
if err != nil {
|
||||
// Log and return on error - perhaps it'll work the next time.
|
||||
logger.LogIf(GlobalContext, err)
|
||||
@@ -1417,6 +1460,8 @@ func (sys *IAMSys) PolicyDBGet(name string, isGroup bool, groups ...string) ([]s
|
||||
return sys.store.PolicyDBGet(name, isGroup, groups...)
|
||||
}
|
||||
|
||||
const sessionPolicyNameExtracted = iampolicy.SessionPolicyName + "-extracted"
|
||||
|
||||
// IsAllowedServiceAccount - checks if the given service account is allowed to perform
|
||||
// actions. The permission of the parent user is checked first
|
||||
func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser string) bool {
|
||||
@@ -1493,12 +1538,12 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
return false
|
||||
}
|
||||
|
||||
if saPolicyClaimStr == "inherited-policy" {
|
||||
if saPolicyClaimStr == inheritedPolicyType {
|
||||
return combinedPolicy.IsAllowed(parentArgs)
|
||||
}
|
||||
|
||||
// Now check if we have a sessionPolicy.
|
||||
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
|
||||
spolicy, ok := args.Claims[sessionPolicyNameExtracted]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
@@ -1530,54 +1575,10 @@ func (sys *IAMSys) IsAllowedServiceAccount(args iampolicy.Args, parentUser strin
|
||||
return combinedPolicy.IsAllowed(parentArgs) && subPolicy.IsAllowed(parentArgs)
|
||||
}
|
||||
|
||||
// IsAllowedLDAPSTS - checks for LDAP specific claims and values
|
||||
func (sys *IAMSys) IsAllowedLDAPSTS(args iampolicy.Args, parentUser string) bool {
|
||||
// parentUser value must match the ldap user in the claim.
|
||||
if parentInClaimIface, ok := args.Claims[ldapUser]; !ok {
|
||||
// no ldapUser claim present reject it.
|
||||
return false
|
||||
} else if parentInClaim, ok := parentInClaimIface.(string); !ok {
|
||||
// not the right type, reject it.
|
||||
return false
|
||||
} else if parentInClaim != parentUser {
|
||||
// ldap claim has been modified maliciously reject it.
|
||||
return false
|
||||
}
|
||||
|
||||
// Check policy for this LDAP user.
|
||||
ldapPolicies, err := sys.PolicyDBGet(parentUser, false, args.Groups...)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(ldapPolicies) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Policies were found, evaluate all of them.
|
||||
availablePoliciesStr, combinedPolicy := sys.store.FilterPolicies(strings.Join(ldapPolicies, ","), "")
|
||||
if availablePoliciesStr == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
hasSessionPolicy, isAllowedSP := isAllowedBySessionPolicy(args)
|
||||
if hasSessionPolicy {
|
||||
return isAllowedSP && combinedPolicy.IsAllowed(args)
|
||||
}
|
||||
|
||||
return combinedPolicy.IsAllowed(args)
|
||||
}
|
||||
|
||||
// IsAllowedSTS is meant for STS based temporary credentials,
|
||||
// which implements claims validation and verification other than
|
||||
// applying policies.
|
||||
func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
// If it is an LDAP request, check that user and group
|
||||
// policies allow the request.
|
||||
if sys.usersSysType == LDAPUsersSysType {
|
||||
return sys.IsAllowedLDAPSTS(args, parentUser)
|
||||
}
|
||||
|
||||
var policies []string
|
||||
roleArn := args.GetRoleArn()
|
||||
if roleArn != "" {
|
||||
@@ -1612,6 +1613,11 @@ func (sys *IAMSys) IsAllowedSTS(args iampolicy.Args, parentUser string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defensive code: Do not allow any operation if no policy is found in the session token
|
||||
if len(policies) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
combinedPolicy, err := sys.store.GetPolicy(strings.Join(policies, ","))
|
||||
if err == errNoSuchPolicy {
|
||||
for _, pname := range policies {
|
||||
@@ -1646,7 +1652,7 @@ func isAllowedBySessionPolicy(args iampolicy.Args) (hasSessionPolicy bool, isAll
|
||||
isAllowed = false
|
||||
|
||||
// Now check if we have a sessionPolicy.
|
||||
spolicy, ok := args.Claims[iampolicy.SessionPolicyName]
|
||||
spolicy, ok := args.Claims[sessionPolicyNameExtracted]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -1686,8 +1692,8 @@ func (sys *IAMSys) GetCombinedPolicy(policies ...string) iampolicy.Policy {
|
||||
// IsAllowed - checks given policy args is allowed to continue the Rest API.
|
||||
func (sys *IAMSys) IsAllowed(args iampolicy.Args) bool {
|
||||
// If opa is configured, use OPA always.
|
||||
if globalPolicyOPA != nil {
|
||||
ok, err := globalPolicyOPA.IsAllowed(args)
|
||||
if authz := newGlobalAuthZPluginFn(); authz != nil {
|
||||
ok, err := authz.IsAllowed(args)
|
||||
if err != nil {
|
||||
logger.LogIf(GlobalContext, err)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
||||
|
||||
peers, _ := newPeerRestClients(globalEndpoints)
|
||||
|
||||
globalHTTPListen.Subscribe(listenCh, ctx.Done(), func(evI interface{}) bool {
|
||||
listenFn := func(evI interface{}) bool {
|
||||
ev, ok := evI.(event.Event)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -138,7 +138,13 @@ func (api objectAPIHandlers) ListenNotificationHandler(w http.ResponseWriter, r
|
||||
}
|
||||
}
|
||||
return rulesMap.MatchSimple(ev.EventName, ev.S3.Object.Key)
|
||||
})
|
||||
}
|
||||
|
||||
err := globalHTTPListen.Subscribe(listenCh, ctx.Done(), listenFn)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrSlowDown), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if bucketName != "" {
|
||||
values.Set(peerRESTListenBucket, bucketName)
|
||||
|
||||
@@ -344,3 +344,96 @@ func Test_localLocker_expireOldLocksExpire(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_localLocker_RUnlock(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(0))
|
||||
// Numbers of unique locks
|
||||
for _, locks := range []int{1, 100, 1000, 1e6} {
|
||||
if testing.Short() && locks > 100 {
|
||||
continue
|
||||
}
|
||||
t.Run(fmt.Sprintf("%d-locks", locks), func(t *testing.T) {
|
||||
// Number of readers per lock...
|
||||
for _, readers := range []int{1, 10, 100} {
|
||||
if locks > 1000 && readers > 1 {
|
||||
continue
|
||||
}
|
||||
if testing.Short() && readers > 10 {
|
||||
continue
|
||||
}
|
||||
t.Run(fmt.Sprintf("%d-read", readers), func(t *testing.T) {
|
||||
l := newLocker()
|
||||
for i := 0; i < locks; i++ {
|
||||
var tmp [16]byte
|
||||
rng.Read(tmp[:])
|
||||
res := []string{hex.EncodeToString(tmp[:])}
|
||||
|
||||
for i := 0; i < readers; i++ {
|
||||
rng.Read(tmp[:])
|
||||
ok, err := l.RLock(context.Background(), dsync.LockArgs{
|
||||
UID: uuid.NewString(),
|
||||
Resources: res,
|
||||
Source: hex.EncodeToString(tmp[:8]),
|
||||
Owner: hex.EncodeToString(tmp[8:]),
|
||||
Quorum: 0,
|
||||
})
|
||||
if !ok || err != nil {
|
||||
t.Fatal("failed:", err, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expire 50%
|
||||
toUnLock := make([]dsync.LockArgs, 0, locks*readers)
|
||||
for k, v := range l.lockMap {
|
||||
for _, lock := range v {
|
||||
if rng.Intn(2) == 0 {
|
||||
toUnLock = append(toUnLock, dsync.LockArgs{Resources: []string{k}, UID: lock.UID})
|
||||
}
|
||||
}
|
||||
}
|
||||
start := time.Now()
|
||||
for _, lock := range toUnLock {
|
||||
ok, err := l.ForceUnlock(context.Background(), lock)
|
||||
if err != nil || !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Logf("Expire 50%% took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
|
||||
|
||||
if len(l.lockUID) == locks*readers {
|
||||
t.Fatalf("objects uids all remain, unlikely")
|
||||
}
|
||||
if len(l.lockMap) == 0 && locks > 10 {
|
||||
t.Fatalf("objects all deleted, 0 remains")
|
||||
}
|
||||
if len(l.lockUID) != locks*readers-len(toUnLock) {
|
||||
t.Fatalf("want %d objects uids all deleted, %d remains", len(l.lockUID), locks*readers-len(toUnLock))
|
||||
}
|
||||
|
||||
toUnLock = toUnLock[:0]
|
||||
for k, v := range l.lockMap {
|
||||
for _, lock := range v {
|
||||
toUnLock = append(toUnLock, dsync.LockArgs{Resources: []string{k}, UID: lock.UID, Owner: lock.Owner})
|
||||
}
|
||||
}
|
||||
start = time.Now()
|
||||
for _, lock := range toUnLock {
|
||||
ok, err := l.RUnlock(nil, lock)
|
||||
if err != nil || !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Logf("Expire rest took: %v. Left: %d/%d", time.Since(start).Round(time.Millisecond), len(l.lockUID), len(l.lockMap))
|
||||
|
||||
if len(l.lockMap) != 0 {
|
||||
t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockMap))
|
||||
}
|
||||
if len(l.lockUID) != 0 {
|
||||
t.Fatalf("objects not deleted, want %d != got %d", 0, len(l.lockUID))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-18
@@ -209,22 +209,7 @@ func (l *lockRESTServer) ForceUnlockHandler(w http.ResponseWriter, r *http.Reque
|
||||
// lockMaintenance loops over all locks and discards locks
|
||||
// that have not been refreshed for some time.
|
||||
func lockMaintenance(ctx context.Context) {
|
||||
// Wait until the object API is ready
|
||||
// no need to start the lock maintenance
|
||||
// if ObjectAPI is not initialized.
|
||||
|
||||
var objAPI ObjectLayer
|
||||
|
||||
for {
|
||||
objAPI = newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if _, ok := objAPI.(*erasureServerPools); !ok {
|
||||
if !globalIsDistErasure {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -239,10 +224,10 @@ func lockMaintenance(ctx context.Context) {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-lkTimer.C:
|
||||
globalLockServer.expireOldLocks(lockValidityDuration)
|
||||
|
||||
// Reset the timer for next cycle.
|
||||
lkTimer.Reset(lockMaintenanceInterval)
|
||||
|
||||
globalLockServer.expireOldLocks(lockValidityDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -20,6 +20,7 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/cli"
|
||||
@@ -68,6 +69,11 @@ var GlobalFlags = []cli.Flag{
|
||||
},
|
||||
}
|
||||
|
||||
var versionFlag = cli.BoolFlag{
|
||||
Name: "version, v",
|
||||
Usage: "print version information",
|
||||
}
|
||||
|
||||
// Help template for minio.
|
||||
var minioHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
@@ -132,14 +138,19 @@ func newApp(name string) *cli.App {
|
||||
Usage: "show help",
|
||||
}
|
||||
|
||||
topLevelFlags := make([]cli.Flag, len(GlobalFlags)+1)
|
||||
copy(topLevelFlags, GlobalFlags)
|
||||
topLevelFlags[len(GlobalFlags)] = versionFlag
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Name = name
|
||||
app.Author = "MinIO, Inc."
|
||||
app.Version = ReleaseTag
|
||||
app.Action = versionAndHelpAction
|
||||
app.Usage = "High Performance Object Storage"
|
||||
app.Description = `Build high performance data infrastructure for machine learning, analytics and application data workloads with MinIO`
|
||||
app.Flags = GlobalFlags
|
||||
app.Flags = topLevelFlags
|
||||
app.HideHelpCommand = true // Hide `help, h` command, we already have `minio --help`.
|
||||
app.HideVersion = true
|
||||
app.Commands = commands
|
||||
app.CustomAppHelpTemplate = minioHelpTemplate
|
||||
app.CommandNotFound = func(ctx *cli.Context, command string) {
|
||||
@@ -159,6 +170,18 @@ func newApp(name string) *cli.App {
|
||||
return app
|
||||
}
|
||||
|
||||
func versionAndHelpAction(ctx *cli.Context) {
|
||||
if ctx.IsSet("version") {
|
||||
console.Printf("%s version %s\n", ctx.App.Name, ReleaseTag)
|
||||
console.Printf("commit: %s\n", CommitID)
|
||||
console.Printf("go version: %s\n", runtime.Version())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cli.ShowAppHelpAndExit(ctx, 1)
|
||||
}
|
||||
|
||||
// Main main for minio server.
|
||||
func Main(args []string) {
|
||||
// Set the minio app name.
|
||||
|
||||
+14
-6
@@ -51,10 +51,12 @@ func newBucketMetacache(bucket string, cleanup bool) *bucketMetacache {
|
||||
if cleanup {
|
||||
// Recursively delete all caches.
|
||||
objAPI := newObjectLayerFn()
|
||||
ez, ok := objAPI.(*erasureServerPools)
|
||||
if ok {
|
||||
ctx := context.Background()
|
||||
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(bucket, slashSeparator))
|
||||
if objAPI != nil {
|
||||
ez, ok := objAPI.(renameAllStorager)
|
||||
if ok {
|
||||
ctx := context.Background()
|
||||
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(bucket, slashSeparator))
|
||||
}
|
||||
}
|
||||
}
|
||||
return &bucketMetacache{
|
||||
@@ -207,9 +209,15 @@ func (b *bucketMetacache) cloneCaches() (map[string]metacache, map[string][]stri
|
||||
// Deletes are performed concurrently.
|
||||
func (b *bucketMetacache) deleteAll() {
|
||||
ctx := context.Background()
|
||||
ez, ok := newObjectLayerFn().(*erasureServerPools)
|
||||
|
||||
objAPI := newObjectLayerFn()
|
||||
if objAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ez, ok := objAPI.(renameAllStorager)
|
||||
if !ok {
|
||||
logger.LogIf(ctx, errors.New("bucketMetacache: expected objAPI to be *erasurePools"))
|
||||
logger.LogIf(ctx, errors.New("bucketMetacache: expected objAPI to be 'renameAllStorager'"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -168,8 +168,10 @@ func (e *metaCacheEntry) isLatestDeletemarker() bool {
|
||||
if !isXL2V1Format(e.metadata) {
|
||||
return false
|
||||
}
|
||||
if meta, _ := isIndexedMetaV2(e.metadata); meta != nil {
|
||||
if meta, _, err := isIndexedMetaV2(e.metadata); meta != nil {
|
||||
return meta.IsLatestDeleteMarker()
|
||||
} else if err != nil {
|
||||
return true
|
||||
}
|
||||
// Fall back...
|
||||
xlMeta, err := e.xlmeta()
|
||||
@@ -438,6 +440,8 @@ func (m metaCacheEntriesSorted) shallowClone() metaCacheEntriesSorted {
|
||||
func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, afterV string) (versions []ObjectInfo) {
|
||||
versions = make([]ObjectInfo, 0, m.len())
|
||||
prevPrefix := ""
|
||||
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
||||
|
||||
for _, entry := range m.o {
|
||||
if entry.isObject() {
|
||||
if delimiter != "" {
|
||||
@@ -473,7 +477,8 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
|
||||
}
|
||||
|
||||
for _, version := range fiVersions {
|
||||
versions = append(versions, version.ToObjectInfo(bucket, entry.name))
|
||||
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
||||
versions = append(versions, version.ToObjectInfo(bucket, entry.name, versioned))
|
||||
}
|
||||
|
||||
continue
|
||||
@@ -509,6 +514,9 @@ func (m *metaCacheEntriesSorted) fileInfoVersions(bucket, prefix, delimiter, aft
|
||||
func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (objects []ObjectInfo) {
|
||||
objects = make([]ObjectInfo, 0, m.len())
|
||||
prevPrefix := ""
|
||||
|
||||
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
||||
|
||||
for _, entry := range m.o {
|
||||
if entry.isObject() {
|
||||
if delimiter != "" {
|
||||
@@ -531,7 +539,8 @@ func (m *metaCacheEntriesSorted) fileInfos(bucket, prefix, delimiter string) (ob
|
||||
|
||||
fi, err := entry.fileInfo(bucket)
|
||||
if err == nil {
|
||||
objects = append(objects, fi.ToObjectInfo(bucket, entry.name))
|
||||
versioned := vcfg != nil && vcfg.Versioned(entry.name)
|
||||
objects = append(objects, fi.ToObjectInfo(bucket, entry.name, versioned))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -686,8 +695,12 @@ func mergeEntryChannels(ctx context.Context, in []chan metaCacheEntry, out chan<
|
||||
}
|
||||
}
|
||||
if best.name > last {
|
||||
out <- *best
|
||||
last = best.name
|
||||
select {
|
||||
case <-ctxDone:
|
||||
return ctx.Err()
|
||||
case out <- *best:
|
||||
last = best.name
|
||||
}
|
||||
} else if serverDebugLog {
|
||||
console.Debugln("mergeEntryChannels: discarding duplicate", best.name, "<=", last)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (m *metacacheManager) initManager() {
|
||||
objAPI = newObjectLayerFn()
|
||||
}
|
||||
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -259,6 +259,286 @@ func (z *erasureServerPools) listPath(ctx context.Context, o *listPathOptions) (
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// listPath will return the requested entries.
|
||||
// If no more entries are in the listing io.EOF is returned,
|
||||
// otherwise nil or an unexpected error is returned.
|
||||
// The listPathOptions given will be checked and modified internally.
|
||||
// Required important fields are Bucket, Prefix, Separator.
|
||||
// Other important fields are Limit, Marker.
|
||||
// List ID always derived from the Marker.
|
||||
func (es *erasureSingle) listPath(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
if err := checkListObjsArgs(ctx, o.Bucket, o.Prefix, o.Marker, es); err != nil {
|
||||
return entries, err
|
||||
}
|
||||
|
||||
// Marker is set validate pre-condition.
|
||||
if o.Marker != "" && o.Prefix != "" {
|
||||
// Marker not common with prefix is not implemented. Send an empty response
|
||||
if !HasPrefix(o.Marker, o.Prefix) {
|
||||
return entries, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
// With max keys of zero we have reached eof, return right here.
|
||||
if o.Limit == 0 {
|
||||
return entries, io.EOF
|
||||
}
|
||||
|
||||
// For delimiter and prefix as '/' we do not list anything at all
|
||||
// along // with the prefix. On a flat namespace with 'prefix'
|
||||
// as '/' we don't have any entries, since all the keys are
|
||||
// of form 'keyName/...'
|
||||
if strings.HasPrefix(o.Prefix, SlashSeparator) {
|
||||
return entries, io.EOF
|
||||
}
|
||||
|
||||
// If delimiter is slashSeparator we must return directories of
|
||||
// the non-recursive scan unless explicitly requested.
|
||||
o.IncludeDirectories = o.Separator == slashSeparator
|
||||
if (o.Separator == slashSeparator || o.Separator == "") && !o.Recursive {
|
||||
o.Recursive = o.Separator != slashSeparator
|
||||
o.Separator = slashSeparator
|
||||
} else {
|
||||
// Default is recursive, if delimiter is set then list non recursive.
|
||||
o.Recursive = true
|
||||
}
|
||||
|
||||
// Decode and get the optional list id from the marker.
|
||||
o.parseMarker()
|
||||
o.BaseDir = baseDirFromPrefix(o.Prefix)
|
||||
o.Transient = o.Transient || isReservedOrInvalidBucket(o.Bucket, false)
|
||||
o.SetFilter()
|
||||
if o.Transient {
|
||||
o.Create = false
|
||||
}
|
||||
|
||||
// We have 2 cases:
|
||||
// 1) Cold listing, just list.
|
||||
// 2) Returning, but with no id. Start async listing.
|
||||
// 3) Returning, with ID, stream from list.
|
||||
//
|
||||
// If we don't have a list id we must ask the server if it has a cache or create a new.
|
||||
if o.ID != "" && !o.Transient {
|
||||
// Create or ping with handout...
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
var c *metacache
|
||||
if rpc == nil {
|
||||
resp := localMetacacheMgr.getBucket(ctx, o.Bucket).findCache(*o)
|
||||
c = &resp
|
||||
} else {
|
||||
c, err = rpc.GetMetacacheListing(ctx, *o)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// Context is canceled, return at once.
|
||||
// request canceled, no entries to return
|
||||
return entries, io.EOF
|
||||
}
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
o.debugln("listPath: got error", err)
|
||||
}
|
||||
o.Transient = true
|
||||
o.Create = false
|
||||
o.ID = mustGetUUID()
|
||||
} else {
|
||||
if c.fileNotFound {
|
||||
// No cache found, no entries found.
|
||||
return entries, io.EOF
|
||||
}
|
||||
if c.status == scanStateError || c.status == scanStateNone {
|
||||
o.ID = ""
|
||||
o.Create = false
|
||||
o.debugln("scan status", c.status, " - waiting a roundtrip to create")
|
||||
} else {
|
||||
// Continue listing
|
||||
o.ID = c.id
|
||||
go func(meta metacache) {
|
||||
// Continuously update while we wait.
|
||||
t := time.NewTicker(metacacheMaxClientWait / 10)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Request is done, stop updating.
|
||||
return
|
||||
case <-t.C:
|
||||
meta.lastHandout = time.Now()
|
||||
if rpc == nil {
|
||||
meta, _ = localMetacacheMgr.updateCacheEntry(meta)
|
||||
}
|
||||
meta, _ = rpc.UpdateMetacacheListing(ctx, meta)
|
||||
}
|
||||
}(*c)
|
||||
}
|
||||
}
|
||||
|
||||
// We have an existing list ID, continue streaming.
|
||||
if o.Create {
|
||||
o.debugln("Creating", o)
|
||||
entries, err = es.listAndSave(ctx, o)
|
||||
if err == nil || err == io.EOF {
|
||||
return entries, err
|
||||
}
|
||||
entries.truncate(0)
|
||||
} else {
|
||||
o.debugln("Resuming", o)
|
||||
entries, err = es.streamMetadataParts(ctx, *o)
|
||||
entries.reuse = true // We read from stream and are not sharing results.
|
||||
if err == nil {
|
||||
return entries, nil
|
||||
}
|
||||
}
|
||||
if IsErr(err, []error{
|
||||
nil,
|
||||
context.Canceled,
|
||||
context.DeadlineExceeded,
|
||||
// io.EOF is expected and should be returned but no need to log it.
|
||||
io.EOF,
|
||||
}...) {
|
||||
// Expected good errors we don't need to return error.
|
||||
return entries, err
|
||||
}
|
||||
entries.truncate(0)
|
||||
go func() {
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
if rpc != nil {
|
||||
ctx, cancel := context.WithTimeout(GlobalContext, 5*time.Second)
|
||||
defer cancel()
|
||||
c, err := rpc.GetMetacacheListing(ctx, *o)
|
||||
if err == nil {
|
||||
c.error = "no longer used"
|
||||
c.status = scanStateError
|
||||
rpc.UpdateMetacacheListing(ctx, *c)
|
||||
}
|
||||
}
|
||||
}()
|
||||
o.ID = ""
|
||||
|
||||
if err != nil {
|
||||
logger.LogIf(ctx, fmt.Errorf("Resuming listing from drives failed %w, proceeding to do raw listing", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Do listing in-place.
|
||||
// Create output for our results.
|
||||
// Create filter for results.
|
||||
o.debugln("Raw List", o)
|
||||
filterCh := make(chan metaCacheEntry, o.Limit)
|
||||
listCtx, cancelList := context.WithCancel(ctx)
|
||||
filteredResults := o.gatherResults(listCtx, filterCh)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
var listErr error
|
||||
|
||||
go func(o listPathOptions) {
|
||||
defer wg.Done()
|
||||
o.Limit = 0
|
||||
listErr = es.listMerged(listCtx, o, filterCh)
|
||||
o.debugln("listMerged returned with", listErr)
|
||||
}(*o)
|
||||
|
||||
entries, err = filteredResults()
|
||||
cancelList()
|
||||
wg.Wait()
|
||||
if listErr != nil && !errors.Is(listErr, context.Canceled) {
|
||||
return entries, listErr
|
||||
}
|
||||
entries.reuse = true
|
||||
truncated := entries.len() > o.Limit || err == nil
|
||||
entries.truncate(o.Limit)
|
||||
if !o.Transient && truncated {
|
||||
if o.ID == "" {
|
||||
entries.listID = mustGetUUID()
|
||||
} else {
|
||||
entries.listID = o.ID
|
||||
}
|
||||
}
|
||||
if !truncated {
|
||||
return entries, io.EOF
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// listMerged will list across all sets and return a merged results stream.
|
||||
// The result channel is closed when no more results are expected.
|
||||
func (es *erasureSingle) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
var listErr error
|
||||
var inputs []chan metaCacheEntry
|
||||
|
||||
innerResults := make(chan metaCacheEntry, 100)
|
||||
inputs = append(inputs, innerResults)
|
||||
|
||||
mu.Lock()
|
||||
listCtx, cancelList := context.WithCancel(ctx)
|
||||
defer cancelList()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := es.listPathInner(listCtx, o, innerResults)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
listErr = err
|
||||
}()
|
||||
mu.Unlock()
|
||||
|
||||
// Do lifecycle filtering.
|
||||
if o.Lifecycle != nil {
|
||||
filterIn := make(chan metaCacheEntry, 10)
|
||||
go filterLifeCycle(ctx, o.Bucket, *o.Lifecycle, o.Retention, filterIn, results)
|
||||
// Replace results.
|
||||
results = filterIn
|
||||
}
|
||||
|
||||
// Gather results to a single channel.
|
||||
err := mergeEntryChannels(ctx, inputs, results, func(existing, other *metaCacheEntry) (replace bool) {
|
||||
// Pick object over directory
|
||||
if existing.isDir() && !other.isDir() {
|
||||
return true
|
||||
}
|
||||
if !existing.isDir() && other.isDir() {
|
||||
return false
|
||||
}
|
||||
eMeta, err := existing.xlmeta()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
oMeta, err := other.xlmeta()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Replace if modtime is newer
|
||||
if !oMeta.latestModtime().Equal(oMeta.latestModtime()) {
|
||||
return oMeta.latestModtime().After(eMeta.latestModtime())
|
||||
}
|
||||
// Use NumVersions as a final tiebreaker.
|
||||
return len(oMeta.versions) > len(eMeta.versions)
|
||||
})
|
||||
|
||||
cancelList()
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listErr != nil {
|
||||
if contextCanceled(ctx) {
|
||||
return nil
|
||||
}
|
||||
if listErr.Error() == io.EOF.Error() {
|
||||
return nil
|
||||
}
|
||||
logger.LogIf(ctx, listErr)
|
||||
return listErr
|
||||
}
|
||||
if contextCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listMerged will list across all sets and return a merged results stream.
|
||||
// The result channel is closed when no more results are expected.
|
||||
func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) error {
|
||||
@@ -274,11 +554,11 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
for _, pool := range z.serverPools {
|
||||
for _, set := range pool.sets {
|
||||
wg.Add(1)
|
||||
results := make(chan metaCacheEntry, 100)
|
||||
inputs = append(inputs, results)
|
||||
innerResults := make(chan metaCacheEntry, 100)
|
||||
inputs = append(inputs, innerResults)
|
||||
go func(i int, set *erasureObjects) {
|
||||
defer wg.Done()
|
||||
err := set.listPath(listCtx, o, results)
|
||||
err := set.listPath(listCtx, o, innerResults)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err == nil {
|
||||
@@ -329,6 +609,7 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if contextCanceled(ctx) {
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -362,6 +643,8 @@ func (z *erasureServerPools) listMerged(ctx context.Context, o listPathOptions,
|
||||
// function closes 'out' and exits.
|
||||
func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle, lr lock.Retention, in <-chan metaCacheEntry, out chan<- metaCacheEntry) {
|
||||
defer close(out)
|
||||
|
||||
vcfg, _ := globalBucketVersioningSys.Get(bucket)
|
||||
for {
|
||||
var obj metaCacheEntry
|
||||
var ok bool
|
||||
@@ -378,7 +661,10 @@ func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle,
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
objInfo := fi.ToObjectInfo(bucket, obj.name)
|
||||
|
||||
versioned := vcfg != nil && vcfg.Versioned(obj.name)
|
||||
|
||||
objInfo := fi.ToObjectInfo(bucket, obj.name, versioned)
|
||||
action := evalActionFromLifecycle(ctx, lc, lr, objInfo, false)
|
||||
switch action {
|
||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction:
|
||||
@@ -395,6 +681,73 @@ func filterLifeCycle(ctx context.Context, bucket string, lc lifecycle.Lifecycle,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *erasureSingle) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
// Use ID as the object name...
|
||||
o.pool = 0
|
||||
o.set = 0
|
||||
saver := es
|
||||
|
||||
// Disconnect from call above, but cancel on exit.
|
||||
listCtx, cancel := context.WithCancel(GlobalContext)
|
||||
saveCh := make(chan metaCacheEntry, metacacheBlockSize)
|
||||
inCh := make(chan metaCacheEntry, metacacheBlockSize)
|
||||
outCh := make(chan metaCacheEntry, o.Limit)
|
||||
|
||||
filteredResults := o.gatherResults(ctx, outCh)
|
||||
|
||||
mc := o.newMetacache()
|
||||
meta := metaCacheRPC{meta: &mc, cancel: cancel, rpc: globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix)), o: *o}
|
||||
|
||||
// Save listing...
|
||||
go func() {
|
||||
if err := saver.saveMetaCacheStream(listCtx, &meta, saveCh); err != nil {
|
||||
meta.setErr(err.Error())
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Do listing...
|
||||
go func(o listPathOptions) {
|
||||
err := es.listMerged(listCtx, o, inCh)
|
||||
if err != nil {
|
||||
meta.setErr(err.Error())
|
||||
}
|
||||
o.debugln("listAndSave: listing", o.ID, "finished with ", err)
|
||||
}(*o)
|
||||
|
||||
// Keep track of when we return since we no longer have to send entries to output.
|
||||
var funcReturned bool
|
||||
var funcReturnedMu sync.Mutex
|
||||
defer func() {
|
||||
funcReturnedMu.Lock()
|
||||
funcReturned = true
|
||||
funcReturnedMu.Unlock()
|
||||
}()
|
||||
// Write listing to results and saver.
|
||||
go func() {
|
||||
var returned bool
|
||||
for entry := range inCh {
|
||||
if !returned {
|
||||
funcReturnedMu.Lock()
|
||||
returned = funcReturned
|
||||
funcReturnedMu.Unlock()
|
||||
outCh <- entry
|
||||
if returned {
|
||||
close(outCh)
|
||||
}
|
||||
}
|
||||
entry.reusable = returned
|
||||
saveCh <- entry
|
||||
}
|
||||
if !returned {
|
||||
close(outCh)
|
||||
}
|
||||
close(saveCh)
|
||||
}()
|
||||
|
||||
return filteredResults()
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) listAndSave(ctx context.Context, o *listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
// Use ID as the object name...
|
||||
o.pool = z.getAvailablePoolIdx(ctx, minioMetaBucket, o.ID, 10<<20)
|
||||
|
||||
+343
-1
@@ -543,6 +543,170 @@ func (er *erasureObjects) streamMetadataParts(ctx context.Context, o listPathOpt
|
||||
}
|
||||
}
|
||||
|
||||
func (es *erasureSingle) streamMetadataParts(ctx context.Context, o listPathOptions) (entries metaCacheEntriesSorted, err error) {
|
||||
retries := 0
|
||||
rpc := globalNotificationSys.restClientFromHash(pathJoin(o.Bucket, o.Prefix))
|
||||
|
||||
for {
|
||||
if contextCanceled(ctx) {
|
||||
return entries, ctx.Err()
|
||||
}
|
||||
|
||||
// If many failures, check the cache state.
|
||||
if retries > 10 {
|
||||
err := o.checkMetacacheState(ctx, rpc)
|
||||
if err != nil {
|
||||
return entries, fmt.Errorf("remote listing canceled: %w", err)
|
||||
}
|
||||
retries = 1
|
||||
}
|
||||
|
||||
const retryDelay = 250 * time.Millisecond
|
||||
// All operations are performed without locks, so we must be careful and allow for failures.
|
||||
// Read metadata associated with the object from a disk.
|
||||
if retries > 0 {
|
||||
_, err := es.disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(0), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Load first part metadata...
|
||||
// Read metadata associated with the object from all disks.
|
||||
fi, metaArr, onlineDisks, err := es.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(0), ObjectOptions{}, true)
|
||||
if err != nil {
|
||||
switch toObjectErr(err, minioMetaBucket, o.objectPath(0)).(type) {
|
||||
case ObjectNotFound:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
case InsufficientReadQuorum:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
default:
|
||||
return entries, fmt.Errorf("reading first part metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
partN, err := o.findFirstPart(fi)
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
if retries == 10 {
|
||||
err := o.checkMetacacheState(ctx, rpc)
|
||||
if err != nil {
|
||||
return entries, fmt.Errorf("remote listing canceled: %w", err)
|
||||
}
|
||||
retries = -1
|
||||
}
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
case errors.Is(err, io.EOF):
|
||||
return entries, io.EOF
|
||||
}
|
||||
|
||||
// We got a stream to start at.
|
||||
loadedPart := 0
|
||||
for {
|
||||
if contextCanceled(ctx) {
|
||||
return entries, ctx.Err()
|
||||
}
|
||||
|
||||
if partN != loadedPart {
|
||||
if retries > 10 {
|
||||
err := o.checkMetacacheState(ctx, rpc)
|
||||
if err != nil {
|
||||
return entries, fmt.Errorf("waiting for next part %d: %w", partN, err)
|
||||
}
|
||||
retries = 1
|
||||
}
|
||||
|
||||
if retries > 0 {
|
||||
// Load from one disk only
|
||||
_, err := es.disk.ReadVersion(ctx, minioMetaBucket,
|
||||
o.objectPath(partN), "", false)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Load partN metadata...
|
||||
fi, metaArr, onlineDisks, err = es.getObjectFileInfo(ctx, minioMetaBucket, o.objectPath(partN), ObjectOptions{}, true)
|
||||
if err != nil {
|
||||
time.Sleep(retryDelay)
|
||||
retries++
|
||||
continue
|
||||
}
|
||||
loadedPart = partN
|
||||
bi, err := getMetacacheBlockInfo(fi, partN)
|
||||
logger.LogIf(ctx, err)
|
||||
if err == nil {
|
||||
if bi.pastPrefix(o.Prefix) {
|
||||
return entries, io.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
werr := es.getObjectWithFileInfo(ctx, minioMetaBucket, o.objectPath(partN), 0,
|
||||
fi.Size, pw, fi, metaArr, onlineDisks)
|
||||
pw.CloseWithError(werr)
|
||||
}()
|
||||
|
||||
tmp := newMetacacheReader(pr)
|
||||
e, err := tmp.filter(o)
|
||||
pr.CloseWithError(err)
|
||||
entries.o = append(entries.o, e.o...)
|
||||
if o.Limit > 0 && entries.len() > o.Limit {
|
||||
entries.truncate(o.Limit)
|
||||
return entries, nil
|
||||
}
|
||||
if err == nil {
|
||||
// We stopped within the listing, we are done for now...
|
||||
return entries, nil
|
||||
}
|
||||
if err != nil && err.Error() != io.EOF.Error() {
|
||||
switch toObjectErr(err, minioMetaBucket, o.objectPath(partN)).(type) {
|
||||
case ObjectNotFound:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
case InsufficientReadQuorum:
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
continue
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
return entries, err
|
||||
}
|
||||
}
|
||||
|
||||
// We finished at the end of the block.
|
||||
// And should not expect any more results.
|
||||
bi, err := getMetacacheBlockInfo(fi, partN)
|
||||
logger.LogIf(ctx, err)
|
||||
if err != nil || bi.EOS {
|
||||
// We are done and there are no more parts.
|
||||
return entries, io.EOF
|
||||
}
|
||||
if bi.endedPrefix(o.Prefix) {
|
||||
// Nothing more for prefix.
|
||||
return entries, io.EOF
|
||||
}
|
||||
partN++
|
||||
retries = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getListQuorum interprets list quorum values and returns appropriate
|
||||
// acceptable quorum expected for list operations
|
||||
func getListQuorum(quorum string, driveCount int) int {
|
||||
@@ -562,6 +726,53 @@ func getListQuorum(quorum string, driveCount int) int {
|
||||
return 3
|
||||
}
|
||||
|
||||
// Will return io.EOF if continuing would not yield more results.
|
||||
func (es *erasureSingle) listPathInner(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
|
||||
defer close(results)
|
||||
o.debugf(color.Green("listPath:")+" with options: %#v", o)
|
||||
|
||||
// How to resolve results.
|
||||
resolver := metadataResolutionParams{
|
||||
dirQuorum: 1,
|
||||
objQuorum: 1,
|
||||
bucket: o.Bucket,
|
||||
}
|
||||
|
||||
// Maximum versions requested for "latest" object
|
||||
// resolution on versioned buckets, this is to be only
|
||||
// used when o.Versioned is false
|
||||
if !o.Versioned {
|
||||
resolver.requestedVersions = 1
|
||||
}
|
||||
|
||||
ctxDone := ctx.Done()
|
||||
return listPathRaw(ctx, listPathRawOptions{
|
||||
disks: []StorageAPI{es.disk},
|
||||
bucket: o.Bucket,
|
||||
path: o.BaseDir,
|
||||
recursive: o.Recursive,
|
||||
filterPrefix: o.FilterPrefix,
|
||||
minDisks: 1,
|
||||
forwardTo: o.Marker,
|
||||
agreed: func(entry metaCacheEntry) {
|
||||
select {
|
||||
case <-ctxDone:
|
||||
case results <- entry:
|
||||
}
|
||||
},
|
||||
partial: func(entries metaCacheEntries, nAgreed int, errs []error) {
|
||||
// Results Disagree :-(
|
||||
entry, ok := entries.resolve(&resolver)
|
||||
if ok {
|
||||
select {
|
||||
case <-ctxDone:
|
||||
case results <- *entry:
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Will return io.EOF if continuing would not yield more results.
|
||||
func (er *erasureObjects) listPath(ctx context.Context, o listPathOptions, results chan<- metaCacheEntry) (err error) {
|
||||
defer close(results)
|
||||
@@ -654,6 +865,133 @@ func (m *metaCacheRPC) setErr(err string) {
|
||||
*m.meta = meta
|
||||
}
|
||||
|
||||
func (es *erasureSingle) saveMetaCacheStream(ctx context.Context, mc *metaCacheRPC, entries <-chan metaCacheEntry) (err error) {
|
||||
o := mc.o
|
||||
o.debugf(color.Green("saveMetaCacheStream:")+" with options: %#v", o)
|
||||
|
||||
metaMu := &mc.mu
|
||||
rpc := mc.rpc
|
||||
cancel := mc.cancel
|
||||
defer func() {
|
||||
o.debugln(color.Green("saveMetaCacheStream:")+"err:", err)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
go mc.setErr(err.Error())
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
defer cancel()
|
||||
// Save continuous updates
|
||||
go func() {
|
||||
var err error
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
var exit bool
|
||||
for !exit {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctx.Done():
|
||||
exit = true
|
||||
}
|
||||
metaMu.Lock()
|
||||
meta := *mc.meta
|
||||
meta, err = o.updateMetacacheListing(meta, rpc)
|
||||
if err == nil && time.Since(meta.lastHandout) > metacacheMaxClientWait {
|
||||
cancel()
|
||||
exit = true
|
||||
meta.status = scanStateError
|
||||
meta.error = fmt.Sprintf("listing canceled since time since last handout was %v ago", time.Since(meta.lastHandout).Round(time.Second))
|
||||
o.debugln(color.Green("saveMetaCacheStream: ") + meta.error)
|
||||
meta, err = o.updateMetacacheListing(meta, rpc)
|
||||
}
|
||||
if err == nil {
|
||||
*mc.meta = meta
|
||||
if meta.status == scanStateError {
|
||||
cancel()
|
||||
exit = true
|
||||
}
|
||||
}
|
||||
metaMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
const retryDelay = 200 * time.Millisecond
|
||||
const maxTries = 5
|
||||
|
||||
// Keep destination...
|
||||
// Write results to disk.
|
||||
bw := newMetacacheBlockWriter(entries, func(b *metacacheBlock) error {
|
||||
// if the block is 0 bytes and its a first block skip it.
|
||||
// skip only this for Transient caches.
|
||||
if len(b.data) == 0 && b.n == 0 && o.Transient {
|
||||
return nil
|
||||
}
|
||||
o.debugln(color.Green("saveMetaCacheStream:")+" saving block", b.n, "to", o.objectPath(b.n))
|
||||
r, err := hash.NewReader(bytes.NewReader(b.data), int64(len(b.data)), "", "", int64(len(b.data)))
|
||||
logger.LogIf(ctx, err)
|
||||
custom := b.headerKV()
|
||||
_, err = es.putMetacacheObject(ctx, o.objectPath(b.n), NewPutObjReader(r), ObjectOptions{
|
||||
UserDefined: custom,
|
||||
})
|
||||
if err != nil {
|
||||
mc.setErr(err.Error())
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
if b.n == 0 {
|
||||
return nil
|
||||
}
|
||||
// Update block 0 metadata.
|
||||
var retries int
|
||||
for {
|
||||
meta := b.headerKV()
|
||||
fi := FileInfo{
|
||||
Metadata: make(map[string]string, len(meta)),
|
||||
}
|
||||
for k, v := range meta {
|
||||
fi.Metadata[k] = v
|
||||
}
|
||||
err := es.updateObjectMeta(ctx, minioMetaBucket, o.objectPath(0), fi, es.disk)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
switch err.(type) {
|
||||
case ObjectNotFound:
|
||||
return err
|
||||
case StorageErr:
|
||||
return err
|
||||
case InsufficientReadQuorum:
|
||||
default:
|
||||
logger.LogIf(ctx, err)
|
||||
}
|
||||
if retries >= maxTries {
|
||||
return err
|
||||
}
|
||||
retries++
|
||||
time.Sleep(retryDelay)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Blocks while consuming entries or an error occurs.
|
||||
err = bw.Close()
|
||||
if err != nil {
|
||||
mc.setErr(err.Error())
|
||||
}
|
||||
metaMu.Lock()
|
||||
defer metaMu.Unlock()
|
||||
if mc.meta.error != "" {
|
||||
return err
|
||||
}
|
||||
// Save success
|
||||
mc.meta.status = scanStateSuccess
|
||||
meta, err := o.updateMetacacheListing(*mc.meta, rpc)
|
||||
if err == nil {
|
||||
*mc.meta = meta
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (er *erasureObjects) saveMetaCacheStream(ctx context.Context, mc *metaCacheRPC, entries <-chan metaCacheEntry) (err error) {
|
||||
o := mc.o
|
||||
o.debugf(color.Green("saveMetaCacheStream:")+" with options: %#v", o)
|
||||
@@ -882,7 +1220,11 @@ func listPathRaw(ctx context.Context, opts listPathRawOptions) (err error) {
|
||||
}
|
||||
|
||||
// fallback only when set.
|
||||
for fd := fallback(werr); fd != nil; {
|
||||
for {
|
||||
fd := fallback(werr)
|
||||
if fd == nil {
|
||||
break
|
||||
}
|
||||
// This fallback is only set when
|
||||
// askDisks is less than total
|
||||
// number of disks per set.
|
||||
|
||||
+2
-2
@@ -155,9 +155,9 @@ func (m *metacache) delete(ctx context.Context) {
|
||||
logger.LogIf(ctx, errors.New("metacache.delete: no object layer"))
|
||||
return
|
||||
}
|
||||
ez, ok := objAPI.(*erasureServerPools)
|
||||
ez, ok := objAPI.(renameAllStorager)
|
||||
if !ok {
|
||||
logger.LogIf(ctx, errors.New("metacache.delete: expected objAPI to be *erasureServerPools"))
|
||||
logger.LogIf(ctx, errors.New("metacache.delete: expected objAPI to be 'renameAllStorager'"))
|
||||
return
|
||||
}
|
||||
ez.renameAll(ctx, minioMetaBucket, metacachePrefixForID(m.bucket, m.id))
|
||||
|
||||
+4
-3
@@ -1365,7 +1365,7 @@ func getMinioHealingMetrics() *MetricsGroup {
|
||||
mg := &MetricsGroup{}
|
||||
mg.RegisterRead(func(_ context.Context) (metrics []Metric) {
|
||||
metrics = make([]Metric, 0, 5)
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
return
|
||||
}
|
||||
bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||
@@ -1604,8 +1604,9 @@ func getBucketUsageMetrics() *MetricsGroup {
|
||||
Value: float64(time.Since(dataUsageInfo.LastUpdate)),
|
||||
})
|
||||
|
||||
bucketReplStats := getAllLatestReplicationStats(dataUsageInfo.BucketsUsage)
|
||||
for bucket, usage := range dataUsageInfo.BucketsUsage {
|
||||
stats := getLatestReplicationStats(bucket, usage)
|
||||
stats := bucketReplStats[bucket]
|
||||
|
||||
quota, _ := globalBucketQuotaSys.Get(ctx, bucket)
|
||||
|
||||
@@ -1816,7 +1817,7 @@ func getClusterStorageMetrics() *MetricsGroup {
|
||||
mg.RegisterRead(func(ctx context.Context) (metrics []Metric) {
|
||||
objLayer := newObjectLayerFn()
|
||||
// Service not initialized yet
|
||||
if objLayer == nil || !globalIsErasure {
|
||||
if objLayer == nil || globalIsGateway {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -132,7 +132,7 @@ func nodeHealthMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
// collects healing specific metrics for MinIO instance in Prometheus specific format
|
||||
// and sends to given channel
|
||||
func healingMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
if !globalIsErasure {
|
||||
if globalIsGateway {
|
||||
return
|
||||
}
|
||||
bgSeq, exists := globalBackgroundHealState.getHealSequenceByToken(bgHealingUUID)
|
||||
@@ -191,7 +191,7 @@ func healingMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
// collects gateway specific metrics for MinIO instance in Prometheus specific format
|
||||
// and sends to given channel
|
||||
func gatewayMetricsPrometheus(ch chan<- prometheus.Metric) {
|
||||
if !globalIsGateway || (globalGatewayName != S3BackendGateway && globalGatewayName != AzureBackendGateway) {
|
||||
if !globalIsGateway || (globalGatewayName != S3BackendGateway) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+11
-13
@@ -190,12 +190,12 @@ func (m *mrfState) healRoutine() {
|
||||
}
|
||||
|
||||
for {
|
||||
idler.Reset(mrfInfoResetInterval)
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case <-idler.C:
|
||||
m.resetMRFInfoIfNoPendingOps()
|
||||
idler.Reset(mrfInfoResetInterval)
|
||||
case setInfo := <-m.setReconnectEvent:
|
||||
// Get the list of objects related the er.set
|
||||
// to which the connected disk belongs.
|
||||
@@ -218,21 +218,19 @@ func (m *mrfState) healRoutine() {
|
||||
|
||||
// Heal objects
|
||||
for _, u := range mrfOperations {
|
||||
if _, err := m.objectAPI.HealObject(m.ctx, u.bucket, u.object, u.versionID, mrfHealingOpts); err != nil {
|
||||
// If not deleted, assume they failed.
|
||||
logger.LogIf(m.ctx, err)
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
m.itemsHealed++
|
||||
m.pendingItems--
|
||||
m.bytesHealed += uint64(u.size)
|
||||
m.pendingBytes -= uint64(u.size)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
_, err := m.objectAPI.HealObject(m.ctx, u.bucket, u.object, u.versionID, mrfHealingOpts)
|
||||
m.mu.Lock()
|
||||
if err == nil {
|
||||
m.itemsHealed++
|
||||
m.bytesHealed += uint64(u.size)
|
||||
}
|
||||
m.pendingItems--
|
||||
m.pendingBytes -= uint64(u.size)
|
||||
delete(m.pendingOps, u)
|
||||
m.mu.Unlock()
|
||||
|
||||
// Log healing error if any
|
||||
logger.LogIf(m.ctx, err)
|
||||
}
|
||||
|
||||
waitForLowHTTPReq()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user