mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 18:45:42 +03:00
Compare commits
178 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2619d1f62 | |||
| 8c70975283 | |||
| 01447d2438 | |||
| 07f31e574c | |||
| 8d223e07fb | |||
| 4041a8727c | |||
| 5f243fde9a | |||
| a0e3f1cc18 | |||
| b1bc641105 | |||
| e0c8738230 | |||
| 9aa24b1920 | |||
| 53d40e41bc | |||
| e88d494775 | |||
| b67f0cf721 | |||
| 46922c71b7 | |||
| 670edb4fcf | |||
| 42d4ab2a0a | |||
| 5e2eb372bf | |||
| cccb37a5ac | |||
| dbf31af6cb | |||
| 93e40c3ab4 | |||
| 8aa0e9ff7c | |||
| bbd6f18afb | |||
| 2a3acc4f24 | |||
| 11507d46da | |||
| f9c62dea55 | |||
| 8c2c92f7af | |||
| 4c71f1b4ec | |||
| 6cd8a372cb | |||
| 953a3e2bbd | |||
| 7cc0c69228 | |||
| f129fd48f2 | |||
| bc4008ced4 | |||
| 526053339b | |||
| 62a35b3e77 | |||
| 39df134204 | |||
| ad4cbce22d | |||
| 90f5e1e5f6 | |||
| aeabac9181 | |||
| b312f13473 | |||
| 727a803bc0 | |||
| d0e443172d | |||
| 60446e7ac0 | |||
| b8544266e5 | |||
| 437dd4e32a | |||
| 447054b841 | |||
| 9bf43e54cd | |||
| 60f8423157 | |||
| 4355ea3c3f | |||
| e30f1ad7bd | |||
| f00c8c4cce | |||
| 703f51164d | |||
| b8dde47d4e | |||
| 7fa3e39f85 | |||
| 4df7a3aa8f | |||
| 64a8f2e554 | |||
| f4fd4ea66d | |||
| 712fe1a8df | |||
| 4a319bedc9 | |||
| bdb3db6dad | |||
| abb385af41 | |||
| 4ee62606e4 | |||
| 079d64c801 | |||
| dcc000ae2c | |||
| c5d19ecebb | |||
| ed29a525b3 | |||
| 020c46cd3c | |||
| 827004cd6d | |||
| 779ec8f0d4 | |||
| 3d0f513ee2 | |||
| 4b6eadbd80 | |||
| 6f47414b23 | |||
| 224a27992a | |||
| 232544e1d8 | |||
| dbcb71828d | |||
| b9196757fd | |||
| b4ac53d157 | |||
| 4952bdb770 | |||
| 00b2ef2932 | |||
| 4536ecfaa4 | |||
| 43a7402968 | |||
| 330dca9a35 | |||
| ddd137d317 | |||
| 06ddd8770e | |||
| 16f8cf1c52 | |||
| 01e520eb23 | |||
| 02f770a0c0 | |||
| 2f4c79bc0f | |||
| 969ee7dfbe | |||
| 5f0b086b05 | |||
| 68b004a48f | |||
| 54ecce66f0 | |||
| 2b008c598b | |||
| 86d02b17cf | |||
| c1a95a70ac | |||
| f246c9053f | |||
| 9cdd204ae4 | |||
| 7b3eb9f7f8 | |||
| d56ef8dbe1 | |||
| a248ed5ff5 | |||
| 5bb31e4883 | |||
| aff2a76d80 | |||
| eddbe6bca2 | |||
| 734d1e320a | |||
| b8dab7b1a9 | |||
| abd6bf060d | |||
| f0d4ef604c | |||
| 2712f75762 | |||
| 4c46668da8 | |||
| 02e93fd6ba | |||
| 366876e98b | |||
| d202fdd022 | |||
| c07e5b49d4 | |||
| 9a39f8ad4d | |||
| 7e0c1c9413 | |||
| 485d833cd7 | |||
| e8a476ef5a | |||
| 267f0ecea2 | |||
| 4ee3434854 | |||
| 0e9854372e | |||
| b5177993b3 | |||
| 55f5c18fd9 | |||
| 8ce101c174 | |||
| 4972735507 | |||
| e6ca6de194 | |||
| cefc43e4da | |||
| 25e34fda5f | |||
| 4208d7af5a | |||
| 8d42f37e4b | |||
| 7cb4b5c636 | |||
| 1615920f48 | |||
| 7ee42b3ff5 | |||
| a6f1e727fb | |||
| c1fc7779ca | |||
| b3ab7546ee | |||
| ad88a81e3d | |||
| c4239ced22 | |||
| f85c28e960 | |||
| f7e176d4ca | |||
| 72a0d14195 | |||
| 6abe4128d7 | |||
| ed5ed7e490 | |||
| 51410c9023 | |||
| 96ca402dcd | |||
| 3da7c9cce3 | |||
| a14e19ec54 | |||
| e091dde041 | |||
| d10bb7e1b6 | |||
| 7ebceacac6 | |||
| 1593cb615d | |||
| 86a41d1631 | |||
| d4157b819c | |||
| e0aceca1b7 | |||
| 87804624fe | |||
| e029f8a9d7 | |||
| 1bc6681176 | |||
| 28322124e2 | |||
| cbfe9de3e7 | |||
| dc86b8d9d4 | |||
| ba70118e2b | |||
| cb1d3e50f7 | |||
| ded0b19d97 | |||
| d0bb3dd136 | |||
| ab7714b01e | |||
| e5b18df6db | |||
| 0abfd1bcb1 | |||
| 6186d11761 | |||
| e8b457e8a6 | |||
| afea40cc0f | |||
| 402b798f1b | |||
| 4759532e90 | |||
| 7f1e1713ab | |||
| b2c5819dbc | |||
| 2b0156b1fc | |||
| f6f0807c86 | |||
| 0c53d86017 | |||
| 6a6ee46d76 | |||
| 974cbb3bb7 |
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Resiliency Functional Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.23.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Build on ${{ matrix.os }}
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
GO111MODULE: on
|
||||||
|
run: |
|
||||||
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
|
make test-resiliency
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -39,3 +39,4 @@ jobs:
|
|||||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
make verify
|
make verify
|
||||||
|
make test-timeout
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
# are turned off - i.e. if ldap="", then ldap server is not enabled for
|
||||||
# the tests.
|
# the tests.
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
ldap: ["", "localhost:389"]
|
ldap: ["", "localhost:389"]
|
||||||
etcd: ["", "http://localhost:2379"]
|
etcd: ["", "http://localhost:2379"]
|
||||||
openid: ["", "http://127.0.0.1:5556/dex"]
|
openid: ["", "http://127.0.0.1:5556/dex"]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: setup-go-step
|
- name: setup-go-step
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22.x
|
go-version: 1.23.x
|
||||||
|
|
||||||
- name: github sha short
|
- name: github sha short
|
||||||
id: vars
|
id: vars
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -40,6 +40,7 @@ jobs:
|
|||||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||||
make test-ilm
|
make test-ilm
|
||||||
|
make test-ilm-transition
|
||||||
|
|
||||||
- name: Test PBAC
|
- name: Test PBAC
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ docker volume rm $(docker volume ls -f dangling=true) || true
|
|||||||
## change working directory
|
## change working directory
|
||||||
cd .github/workflows/mint
|
cd .github/workflows/mint
|
||||||
|
|
||||||
|
## always pull latest
|
||||||
|
docker pull docker.io/minio/mint:edge
|
||||||
|
|
||||||
docker-compose -f minio-${MODE}.yaml up -d
|
docker-compose -f minio-${MODE}.yaml up -d
|
||||||
sleep 1m
|
sleep 1m
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: [1.22.x]
|
go-version: [1.23.x]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22.7
|
go-version: 1.23.5
|
||||||
- name: Get official govulncheck
|
- name: Get official govulncheck
|
||||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
+51
-23
@@ -1,36 +1,64 @@
|
|||||||
linters-settings:
|
version: "2"
|
||||||
gofumpt:
|
|
||||||
simplify: true
|
|
||||||
|
|
||||||
misspell:
|
|
||||||
locale: US
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
checks: ['all', '-ST1005', '-ST1000', '-SA4000', '-SA9004', '-SA1019', '-SA1008', '-U1000', '-ST1016']
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
default: none
|
||||||
enable:
|
enable:
|
||||||
- durationcheck
|
- durationcheck
|
||||||
|
- forcetypeassert
|
||||||
- gocritic
|
- gocritic
|
||||||
- gofumpt
|
|
||||||
- goimports
|
|
||||||
- gomodguard
|
- gomodguard
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- revive
|
- revive
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- tenv
|
|
||||||
- typecheck
|
|
||||||
- unconvert
|
- unconvert
|
||||||
- unused
|
- unused
|
||||||
|
- usetesting
|
||||||
|
- whitespace
|
||||||
|
settings:
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
staticcheck:
|
||||||
|
checks:
|
||||||
|
- all
|
||||||
|
- -SA1008
|
||||||
|
- -SA1019
|
||||||
|
- -SA4000
|
||||||
|
- -SA9004
|
||||||
|
- -ST1000
|
||||||
|
- -ST1005
|
||||||
|
- -ST1016
|
||||||
|
- -U1000
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- forcetypeassert
|
||||||
|
path: _test\.go
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: 'empty-block:'
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: 'unused-parameter:'
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: 'dot-imports:'
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: should have a package comment
|
||||||
|
- path: (.+)\.go$
|
||||||
|
text: error strings should not be capitalized or end with punctuation or a newline
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
issues:
|
issues:
|
||||||
exclude-use-default: false
|
max-issues-per-linter: 100
|
||||||
exclude:
|
max-same-issues: 100
|
||||||
- "empty-block:"
|
formatters:
|
||||||
- "unused-parameter:"
|
enable:
|
||||||
- "dot-imports:"
|
- gofumpt
|
||||||
- should have a package comment
|
- goimports
|
||||||
- error strings should not be capitalized or end with punctuation or a newline
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
|||||||
+10
-7
@@ -1,11 +1,5 @@
|
|||||||
[files]
|
[files]
|
||||||
extend-exclude = [
|
extend-exclude = [".git/", "docs/", "CREDITS", "go.mod", "go.sum"]
|
||||||
".git/",
|
|
||||||
"docs/",
|
|
||||||
"CREDITS",
|
|
||||||
"go.mod",
|
|
||||||
"go.sum",
|
|
||||||
]
|
|
||||||
ignore-hidden = false
|
ignore-hidden = false
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
@@ -20,6 +14,7 @@ extend-ignore-re = [
|
|||||||
'http\.Header\{"X-Amz-Server-Side-Encryptio":',
|
'http\.Header\{"X-Amz-Server-Side-Encryptio":',
|
||||||
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
|
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
|
||||||
"ERRO:",
|
"ERRO:",
|
||||||
|
"(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # ignore line
|
||||||
]
|
]
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
@@ -40,3 +35,11 @@ extend-ignore-re = [
|
|||||||
"TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned"
|
"TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned"
|
||||||
"thr" = "thr"
|
"thr" = "thr"
|
||||||
"toi" = "toi"
|
"toi" = "toi"
|
||||||
|
|
||||||
|
[type.go]
|
||||||
|
extend-ignore-identifiers-re = [
|
||||||
|
# Variants of `typ` used to mean `type` in golang as it is otherwise a
|
||||||
|
# keyword - some of these (like typ1 -> type1) can be fixed, but probably
|
||||||
|
# not worth the effort.
|
||||||
|
"[tT]yp[0-9]*",
|
||||||
|
]
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22-alpine as build
|
FROM golang:1.23-alpine as build
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RELEASE
|
ARG RELEASE
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22-alpine AS build
|
FROM golang:1.23-alpine AS build
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RELEASE
|
ARG RELEASE
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
FROM golang:1.22-alpine AS build
|
|
||||||
|
|
||||||
ARG TARGETARCH
|
|
||||||
ARG RELEASE
|
|
||||||
|
|
||||||
ENV GOPATH=/go
|
|
||||||
ENV CGO_ENABLED=0
|
|
||||||
|
|
||||||
# Install curl and minisign
|
|
||||||
RUN apk add -U --no-cache ca-certificates && \
|
|
||||||
apk add -U --no-cache curl && \
|
|
||||||
go install aead.dev/minisign/cmd/minisign@v0.2.1
|
|
||||||
|
|
||||||
# Download minio binary and signature files
|
|
||||||
RUN curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips -o /go/bin/minio && \
|
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.minisig -o /go/bin/minio.minisig && \
|
|
||||||
curl -s -q https://dl.min.io/server/minio/release/linux-${TARGETARCH}/archive/minio.${RELEASE}.fips.sha256sum -o /go/bin/minio.sha256sum && \
|
|
||||||
chmod +x /go/bin/minio
|
|
||||||
|
|
||||||
# Download mc binary and signature files
|
|
||||||
RUN curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips -o /go/bin/mc && \
|
|
||||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.minisig -o /go/bin/mc.minisig && \
|
|
||||||
curl -s -q https://dl.min.io/client/mc/release/linux-${TARGETARCH}/mc.fips.sha256sum -o /go/bin/mc.sha256sum && \
|
|
||||||
chmod +x /go/bin/mc
|
|
||||||
|
|
||||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
|
||||||
curl -L -s -q https://github.com/moparisthebest/static-curl/releases/latest/download/curl-${TARGETARCH} -o /go/bin/curl; \
|
|
||||||
chmod +x /go/bin/curl; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify binary signature using public key "RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGavRUN"
|
|
||||||
RUN minisign -Vqm /go/bin/minio -x /go/bin/minio.minisig -P RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav
|
|
||||||
|
|
||||||
FROM registry.access.redhat.com/ubi9/ubi-micro:latest
|
|
||||||
|
|
||||||
ARG RELEASE
|
|
||||||
|
|
||||||
LABEL name="MinIO" \
|
|
||||||
vendor="MinIO Inc <dev@min.io>" \
|
|
||||||
maintainer="MinIO Inc <dev@min.io>" \
|
|
||||||
version="${RELEASE}" \
|
|
||||||
release="${RELEASE}" \
|
|
||||||
summary="MinIO is a High Performance Object Storage, API compatible with Amazon S3 cloud storage service." \
|
|
||||||
description="MinIO object storage is fundamentally different. Designed for performance and the S3 API, it is 100% open-source. MinIO is ideal for large, private cloud environments with stringent security requirements and delivers mission-critical availability across a diverse range of workloads."
|
|
||||||
|
|
||||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
|
||||||
MINIO_SECRET_KEY_FILE=secret_key \
|
|
||||||
MINIO_ROOT_USER_FILE=access_key \
|
|
||||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
|
||||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
|
||||||
MINIO_UPDATE_MINISIGN_PUBKEY="RWTx5Zr1tiHQLwG9keckT0c45M3AGeHD6IvimQHpyRywVWGbP1aVSGav" \
|
|
||||||
MINIO_CONFIG_ENV_FILE=config.env
|
|
||||||
|
|
||||||
RUN chmod -R 777 /usr/bin
|
|
||||||
|
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=build /go/bin/minio* /usr/bin/
|
|
||||||
COPY --from=build /go/bin/mc* /usr/bin/
|
|
||||||
COPY --from=build /go/bin/cur* /usr/bin/
|
|
||||||
|
|
||||||
COPY CREDITS /licenses/CREDITS
|
|
||||||
COPY LICENSE /licenses/LICENSE
|
|
||||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
|
||||||
|
|
||||||
EXPOSE 9000
|
|
||||||
VOLUME ["/data"]
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
|
||||||
CMD ["minio"]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22-alpine AS build
|
FROM golang:1.23-alpine AS build
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RELEASE
|
ARG RELEASE
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ PWD := $(shell pwd)
|
|||||||
GOPATH := $(shell go env GOPATH)
|
GOPATH := $(shell go env GOPATH)
|
||||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||||
|
|
||||||
GOARCH := $(shell go env GOARCH)
|
GOOS ?= $(shell go env GOOS)
|
||||||
GOOS := $(shell go env GOOS)
|
GOARCH ?= $(shell go env GOARCH)
|
||||||
|
|
||||||
VERSION ?= $(shell git describe --tags)
|
VERSION ?= $(shell git describe --tags)
|
||||||
REPO ?= quay.io/minio
|
REPO ?= quay.io/minio
|
||||||
@@ -24,7 +24,7 @@ help: ## print this help
|
|||||||
getdeps: ## fetch necessary dependencies
|
getdeps: ## fetch necessary dependencies
|
||||||
@mkdir -p ${GOPATH}/bin
|
@mkdir -p ${GOPATH}/bin
|
||||||
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR)
|
@echo "Installing golangci-lint" && curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOLANGCI_DIR)
|
||||||
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.1.10-0.20240227114326-6d6f813fff1b
|
@echo "Installing msgp" && go install -v github.com/tinylib/msgp@v1.2.5
|
||||||
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
|
@echo "Installing stringer" && go install -v golang.org/x/tools/cmd/stringer@latest
|
||||||
|
|
||||||
crosscompile: ## cross compile minio
|
crosscompile: ## cross compile minio
|
||||||
@@ -60,6 +60,10 @@ test-ilm: install-race
|
|||||||
@echo "Running ILM tests"
|
@echo "Running ILM tests"
|
||||||
@env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh
|
@env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh
|
||||||
|
|
||||||
|
test-ilm-transition: install-race
|
||||||
|
@echo "Running ILM tiering tests with healing"
|
||||||
|
@env bash $(PWD)/docs/bucket/lifecycle/setup_ilm_transition.sh
|
||||||
|
|
||||||
test-pbac: install-race
|
test-pbac: install-race
|
||||||
@echo "Running bucket policies tests"
|
@echo "Running bucket policies tests"
|
||||||
@env bash $(PWD)/docs/iam/policies/pbac-tests.sh
|
@env bash $(PWD)/docs/iam/policies/pbac-tests.sh
|
||||||
@@ -145,6 +149,10 @@ test-multipart: install-race ## test multipart
|
|||||||
@echo "Test multipart behavior when part files are missing"
|
@echo "Test multipart behavior when part files are missing"
|
||||||
@(env bash $(PWD)/buildscripts/multipart-quorum-test.sh)
|
@(env bash $(PWD)/buildscripts/multipart-quorum-test.sh)
|
||||||
|
|
||||||
|
test-timeout: install-race ## test multipart
|
||||||
|
@echo "Test server timeout"
|
||||||
|
@(env bash $(PWD)/buildscripts/test-timeout.sh)
|
||||||
|
|
||||||
verify: install-race ## verify minio various setups
|
verify: install-race ## verify minio various setups
|
||||||
@echo "Verifying build with race"
|
@echo "Verifying build with race"
|
||||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||||
@@ -172,7 +180,7 @@ build-debugging:
|
|||||||
|
|
||||||
build: checks build-debugging ## builds minio to $(PWD)
|
build: checks build-debugging ## builds minio to $(PWD)
|
||||||
@echo "Building minio binary to './minio'"
|
@echo "Building minio binary to './minio'"
|
||||||
@CGO_ENABLED=0 go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
|
||||||
hotfix-vars:
|
hotfix-vars:
|
||||||
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
|
$(eval LDFLAGS := $(shell MINIO_RELEASE="RELEASE" MINIO_HOTFIX="hotfix.$(shell git rev-parse --short HEAD)" go run buildscripts/gen-ldflags.go $(shell git describe --tags --abbrev=0 | \
|
||||||
@@ -180,9 +188,9 @@ hotfix-vars:
|
|||||||
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
$(eval VERSION := $(shell git describe --tags --abbrev=0).hotfix.$(shell git rev-parse --short HEAD))
|
||||||
|
|
||||||
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
||||||
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.1/pkger_2.3.1_linux_amd64.deb
|
@wget -q -c https://github.com/minio/pkger/releases/download/v2.3.10/pkger_2.3.10_linux_amd64.deb
|
||||||
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.0.1/linux-systemd/distributed/minio.service
|
@wget -q -c https://raw.githubusercontent.com/minio/minio-service/v1.1.0/linux-systemd/distributed/minio.service
|
||||||
@sudo apt install ./pkger_2.3.1_linux_amd64.deb --yes
|
@sudo apt install ./pkger_2.3.10_linux_amd64.deb --yes
|
||||||
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
|
@mkdir -p minio-release/$(GOOS)-$(GOARCH)/archive
|
||||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
|
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio
|
||||||
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
|
@cp -af ./minio minio-release/$(GOOS)-$(GOARCH)/minio.$(VERSION)
|
||||||
@@ -192,11 +200,11 @@ hotfix: hotfix-vars clean install ## builds minio binary with hotfix tags
|
|||||||
@pkger -r $(VERSION) --ignore
|
@pkger -r $(VERSION) --ignore
|
||||||
|
|
||||||
hotfix-push: hotfix
|
hotfix-push: hotfix
|
||||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/
|
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
|
||||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive
|
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-0.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
|
||||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/
|
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/
|
||||||
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-amd64/archive
|
@scp -q -r minio-release/$(GOOS)-$(GOARCH)/* minio@dl-1.minio.io:~/releases/server/minio/hotfixes/linux-$(GOOS)/archive
|
||||||
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-amd64/archive/minio.$(VERSION)"
|
@echo "Published new hotfix binaries at https://dl.min.io/server/minio/hotfixes/linux-$(GOOS)/archive/minio.$(VERSION)"
|
||||||
|
|
||||||
docker-hotfix-push: docker-hotfix
|
docker-hotfix-push: docker-hotfix
|
||||||
@docker push -q $(TAG) && echo "Published new container $(TAG)"
|
@docker push -q $(TAG) && echo "Published new container $(TAG)"
|
||||||
@@ -209,6 +217,10 @@ docker: build ## builds minio docker container
|
|||||||
@echo "Building minio docker image '$(TAG)'"
|
@echo "Building minio docker image '$(TAG)'"
|
||||||
@docker build -q --no-cache -t $(TAG) . -f Dockerfile
|
@docker build -q --no-cache -t $(TAG) . -f Dockerfile
|
||||||
|
|
||||||
|
test-resiliency: build
|
||||||
|
@echo "Running resiliency tests"
|
||||||
|
@(DOCKER_COMPOSE_FILE=$(PWD)/docs/resiliency/docker-compose.yaml env bash $(PWD)/docs/resiliency/resiliency-tests.sh)
|
||||||
|
|
||||||
install-race: checks build-debugging ## builds minio to $(PWD)
|
install-race: checks build-debugging ## builds minio to $(PWD)
|
||||||
@echo "Building minio binary with -race to './minio'"
|
@echo "Building minio binary with -race to './minio'"
|
||||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/minio 1>/dev/null
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[](https://min.io)
|
[](https://min.io)
|
||||||
|
|
||||||
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads.
|
MinIO is a High Performance Object Storage released under GNU Affero General Public License v3.0. It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure for machine learning, analytics and application data workloads. To learn more about what MinIO is doing for AI storage, go to [AI storage documentation](https://min.io/solutions/object-storage-for-ai).
|
||||||
|
|
||||||
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
|
This README provides quickstart instructions on running MinIO on bare metal hardware, including container-based installations. For Kubernetes environments, use the [MinIO Kubernetes Operator](https://github.com/minio/operator/blob/master/README.md).
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ The following table lists supported architectures. Replace the `wget` URL with t
|
|||||||
| 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> |
|
| 64-bit Intel/AMD | <https://dl.min.io/server/minio/release/linux-amd64/minio> |
|
||||||
| 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> |
|
| 64-bit ARM | <https://dl.min.io/server/minio/release/linux-arm64/minio> |
|
||||||
| 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> |
|
| 64-bit PowerPC LE (ppc64le) | <https://dl.min.io/server/minio/release/linux-ppc64le/minio> |
|
||||||
| IBM Z-Series (S390X) | <https://dl.min.io/server/minio/release/linux-s390x/minio> |
|
|
||||||
|
|
||||||
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
The MinIO deployment starts using default root credentials `minioadmin:minioadmin`. You can test the deployment using the MinIO Console, an embedded web-based object browser built into MinIO Server. Point a web browser running on the host machine to <http://127.0.0.1:9000> and log in with the root credentials. You can use the Browser to create buckets, upload objects, and browse the contents of the MinIO server.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#
|
#
|
||||||
# This script assumes that LDAP server is at:
|
# This script assumes that LDAP server is at:
|
||||||
#
|
#
|
||||||
# `localhost:1389`
|
# `localhost:389`
|
||||||
#
|
#
|
||||||
# if this is not the case, set the environment variable
|
# if this is not the case, set the environment variable
|
||||||
# `_MINIO_LDAP_TEST_SERVER`.
|
# `_MINIO_LDAP_TEST_SERVER`.
|
||||||
@@ -41,7 +41,7 @@ __init__() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
|
if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
|
||||||
export _MINIO_LDAP_TEST_SERVER=localhost:1389
|
export _MINIO_LDAP_TEST_SERVER=localhost:389
|
||||||
echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
|
echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ create_iam_content_in_old_minio() {
|
|||||||
mc alias set old-minio http://localhost:9000 minioadmin minioadmin
|
mc alias set old-minio http://localhost:9000 minioadmin minioadmin
|
||||||
mc ready old-minio
|
mc ready old-minio
|
||||||
mc idp ldap add old-minio \
|
mc idp ldap add old-minio \
|
||||||
server_addr=localhost:1389 \
|
server_addr=localhost:389 \
|
||||||
server_insecure=on \
|
server_insecure=on \
|
||||||
lookup_bind_dn=cn=admin,dc=min,dc=io \
|
lookup_bind_dn=cn=admin,dc=min,dc=io \
|
||||||
lookup_bind_password=admin \
|
lookup_bind_password=admin \
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -n "$TEST_DEBUG" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||||
|
MINIO_CONFIG_DIR="$WORK_DIR/.minio"
|
||||||
|
MINIO=("$PWD/minio" --config-dir "$MINIO_CONFIG_DIR" server)
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$PWD/minio" ]; then
|
||||||
|
echo "minio executable binary not found in current directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap 'catch $LINENO' ERR
|
||||||
|
|
||||||
|
function purge() {
|
||||||
|
rm -rf "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# shellcheck disable=SC2120
|
||||||
|
catch() {
|
||||||
|
if [ $# -ne 0 ]; then
|
||||||
|
echo "error on line $1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cleaning up instances of MinIO"
|
||||||
|
pkill minio || true
|
||||||
|
pkill -9 minio || true
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
if [ $# -ne 0 ]; then
|
||||||
|
exit $#
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
catch
|
||||||
|
|
||||||
|
function gen_put_request() {
|
||||||
|
hdr_sleep=$1
|
||||||
|
body_sleep=$2
|
||||||
|
|
||||||
|
echo "PUT /testbucket/testobject HTTP/1.1"
|
||||||
|
sleep $hdr_sleep
|
||||||
|
echo "Host: foo-header"
|
||||||
|
echo "User-Agent: curl/8.2.1"
|
||||||
|
echo "Accept: */*"
|
||||||
|
echo "Content-Length: 30"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
sleep $body_sleep
|
||||||
|
echo "random line 0"
|
||||||
|
echo "random line 1"
|
||||||
|
echo ""
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_put_object_request() {
|
||||||
|
hdr_timeout=$1
|
||||||
|
body_timeout=$2
|
||||||
|
|
||||||
|
start=$(date +%s)
|
||||||
|
timeout 5m bash -c "gen_put_request $hdr_timeout $body_timeout | netcat 127.0.0.1 $start_port | read" || return -1
|
||||||
|
[ $(($(date +%s) - start)) -gt $((srv_hdr_timeout + srv_idle_timeout + 1)) ] && return -1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_minio_with_timeout() {
|
||||||
|
start_port=$1
|
||||||
|
|
||||||
|
export MINIO_ROOT_USER=minio
|
||||||
|
export MINIO_ROOT_PASSWORD=minio123
|
||||||
|
export MC_HOST_minio="http://minio:minio123@127.0.0.1:${start_port}/"
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
|
||||||
|
mkdir ${WORK_DIR}
|
||||||
|
C_PWD=${PWD}
|
||||||
|
if [ ! -x "$PWD/mc" ]; then
|
||||||
|
MC_BUILD_DIR="mc-$RANDOM"
|
||||||
|
if ! git clone --quiet https://github.com/minio/mc "$MC_BUILD_DIR"; then
|
||||||
|
echo "failed to download https://github.com/minio/mc"
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
(cd "${MC_BUILD_DIR}" && go build -o "$C_PWD/mc")
|
||||||
|
|
||||||
|
# remove mc source.
|
||||||
|
purge "${MC_BUILD_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${MINIO[@]}" --address ":$start_port" --read-header-timeout ${srv_hdr_timeout}s --idle-timeout ${srv_idle_timeout}s "${WORK_DIR}/disk/" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
disown $pid
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||||
|
echo "server1 log:"
|
||||||
|
cat "${WORK_DIR}/server1.log"
|
||||||
|
echo "FAILED"
|
||||||
|
purge "$WORK_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
"${PWD}/mc" mb minio/testbucket
|
||||||
|
"${PWD}/mc" anonymous set public minio/testbucket
|
||||||
|
|
||||||
|
# slow header writing
|
||||||
|
send_put_object_request 20 0 && exit -1
|
||||||
|
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
|
||||||
|
|
||||||
|
# quick header write and slow bodywrite
|
||||||
|
send_put_object_request 0 40 && exit -1
|
||||||
|
"${PWD}/mc" stat minio/testbucket/testobject && exit -1
|
||||||
|
|
||||||
|
# quick header and body write
|
||||||
|
send_put_object_request 1 1 || exit -1
|
||||||
|
"${PWD}/mc" stat minio/testbucket/testobject || exit -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
export start_port=$(shuf -i 10000-65000 -n 1)
|
||||||
|
export srv_hdr_timeout=5
|
||||||
|
export srv_idle_timeout=5
|
||||||
|
export -f gen_put_request
|
||||||
|
|
||||||
|
test_minio_with_timeout ${start_port}
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/mux"
|
"github.com/minio/mux"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
@@ -980,7 +981,6 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
|||||||
rpt.SetStatus(bucket, "", err)
|
rpt.SetStatus(bucket, "", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
|
rptData, err := json.Marshal(rpt.BucketMetaImportErrs)
|
||||||
@@ -1039,7 +1039,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
|
|||||||
}
|
}
|
||||||
if len(diffCh) == 0 {
|
if len(diffCh) == 0 {
|
||||||
// Flush if nothing is queued
|
// Flush if nothing is queued
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
case <-keepAliveTicker.C:
|
case <-keepAliveTicker.C:
|
||||||
if len(diffCh) > 0 {
|
if len(diffCh) > 0 {
|
||||||
@@ -1048,7 +1048,7 @@ func (a adminAPIHandlers) ReplicationDiffHandler(w http.ResponseWriter, r *http.
|
|||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1098,7 +1098,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
if len(mrfCh) == 0 {
|
if len(mrfCh) == 0 {
|
||||||
// Flush if nothing is queued
|
// Flush if nothing is queued
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
case <-keepAliveTicker.C:
|
case <-keepAliveTicker.C:
|
||||||
if len(mrfCh) > 0 {
|
if len(mrfCh) > 0 {
|
||||||
@@ -1107,7 +1107,7 @@ func (a adminAPIHandlers) ReplicationMRFHandler(w http.ResponseWriter, r *http.R
|
|||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,7 +125,6 @@ func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||||
|
|
||||||
var validationErr ldap.Validation
|
var validationErr ldap.Validation
|
||||||
if errors.As(err, &validationErr) {
|
if errors.As(err, &validationErr) {
|
||||||
// If we got an LDAP validation error, we need to send appropriate
|
// If we got an LDAP validation error, we need to send appropriate
|
||||||
@@ -416,7 +415,6 @@ func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||||
|
|
||||||
var validationErr ldap.Validation
|
var validationErr ldap.Validation
|
||||||
if errors.As(err, &validationErr) {
|
if errors.As(err, &validationErr) {
|
||||||
// If we got an LDAP validation error, we need to send appropriate
|
// If we got an LDAP validation error, we need to send appropriate
|
||||||
|
|||||||
@@ -214,10 +214,7 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we are creating svc account for request sender.
|
// Check if we are creating svc account for request sender.
|
||||||
isSvcAccForRequestor := false
|
isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
|
||||||
if targetUser == requestorUser || targetUser == requestorParentUser {
|
|
||||||
isSvcAccForRequestor = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
targetGroups []string
|
targetGroups []string
|
||||||
@@ -345,7 +342,7 @@ func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.R
|
|||||||
Name: newCred.Name,
|
Name: newCred.Name,
|
||||||
Description: newCred.Description,
|
Description: newCred.Description,
|
||||||
Claims: opts.claims,
|
Claims: opts.claims,
|
||||||
SessionPolicy: createReq.Policy,
|
SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
|
||||||
Status: auth.AccountOn,
|
Status: auth.AccountOn,
|
||||||
Expiration: createReq.Expiration,
|
Expiration: createReq.Expiration,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -258,8 +258,8 @@ func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request)
|
|||||||
// concurrent rebalance-start commands.
|
// concurrent rebalance-start commands.
|
||||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
if proxyEp.Endpoint.Host == ep.Host {
|
if proxyEp.Host == ep.Host {
|
||||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,8 +329,8 @@ func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request
|
|||||||
// pools may temporarily have out of date info on the others.
|
// pools may temporarily have out of date info on the others.
|
||||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
if proxyEp.Endpoint.Host == ep.Host {
|
if proxyEp.Host == ep.Host {
|
||||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,8 +383,8 @@ func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||||
if proxyEp.Endpoint.Host == host && !proxyEp.IsLocal {
|
if proxyEp.Host == host && !proxyEp.IsLocal {
|
||||||
if proxyRequestByNodeIndex(ctx, w, r, nodeIdx) {
|
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-69
@@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/mux"
|
"github.com/minio/mux"
|
||||||
xldap "github.com/minio/pkg/v3/ldap"
|
xldap "github.com/minio/pkg/v3/ldap"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
@@ -65,6 +66,17 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This API only supports removal of internal users not service accounts.
|
||||||
|
ok, _, err = globalIAMSys.IsServiceAccount(accessKey)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// When the user is root credential you are not allowed to
|
// When the user is root credential you are not allowed to
|
||||||
// remove the root user. Also you cannot delete yourself.
|
// remove the root user. Also you cannot delete yourself.
|
||||||
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
||||||
@@ -185,12 +197,7 @@ func (a adminAPIHandlers) GetUserInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDenyOnly := false
|
checkDenyOnly := name == cred.AccessKey
|
||||||
if name == cred.AccessKey {
|
|
||||||
// Check that there is no explicit deny - otherwise it's allowed
|
|
||||||
// to view one's own info.
|
|
||||||
checkDenyOnly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !globalIAMSys.IsAllowed(policy.Args{
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
@@ -481,12 +488,7 @@ func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDenyOnly := false
|
checkDenyOnly := accessKey == cred.AccessKey
|
||||||
if accessKey == cred.AccessKey {
|
|
||||||
// Check that there is no explicit deny - otherwise it's allowed
|
|
||||||
// to change one's own password.
|
|
||||||
checkDenyOnly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !globalIAMSys.IsAllowed(policy.Args{
|
if !globalIAMSys.IsAllowed(policy.Args{
|
||||||
AccountName: cred.AccessKey,
|
AccountName: cred.AccessKey,
|
||||||
@@ -677,10 +679,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we are creating svc account for request sender.
|
// Check if we are creating svc account for request sender.
|
||||||
isSvcAccForRequestor := false
|
isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
|
||||||
if targetUser == requestorUser || targetUser == requestorParentUser {
|
|
||||||
isSvcAccForRequestor = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are creating svc account for request sender, ensure
|
// If we are creating svc account for request sender, ensure
|
||||||
// that targetUser is a real user (i.e. not derived
|
// that targetUser is a real user (i.e. not derived
|
||||||
@@ -771,7 +770,7 @@ func (a adminAPIHandlers) AddServiceAccount(w http.ResponseWriter, r *http.Reque
|
|||||||
Name: newCred.Name,
|
Name: newCred.Name,
|
||||||
Description: newCred.Description,
|
Description: newCred.Description,
|
||||||
Claims: opts.claims,
|
Claims: opts.claims,
|
||||||
SessionPolicy: createReq.Policy,
|
SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
|
||||||
Status: auth.AccountOn,
|
Status: auth.AccountOn,
|
||||||
Expiration: createReq.Expiration,
|
Expiration: createReq.Expiration,
|
||||||
},
|
},
|
||||||
@@ -895,7 +894,7 @@ func (a adminAPIHandlers) UpdateServiceAccount(w http.ResponseWriter, r *http.Re
|
|||||||
Status: opts.status,
|
Status: opts.status,
|
||||||
Name: opts.name,
|
Name: opts.name,
|
||||||
Description: opts.description,
|
Description: opts.description,
|
||||||
SessionPolicy: updateReq.NewPolicy,
|
SessionPolicy: madmin.SRSessionPolicy(updateReq.NewPolicy),
|
||||||
Expiration: updateReq.NewExpiration,
|
Expiration: updateReq.NewExpiration,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1244,6 +1243,7 @@ func (a adminAPIHandlers) ListAccessKeysBulk(w http.ResponseWriter, r *http.Requ
|
|||||||
for user := range users {
|
for user := range users {
|
||||||
checkedUserList = append(checkedUserList, user)
|
checkedUserList = append(checkedUserList, user)
|
||||||
}
|
}
|
||||||
|
checkedUserList = append(checkedUserList, globalActiveCred.AccessKey)
|
||||||
} else {
|
} else {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
// Validate the user
|
// Validate the user
|
||||||
@@ -1441,7 +1441,12 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
var buf []byte
|
var buf []byte
|
||||||
switch {
|
switch {
|
||||||
case accountName == globalActiveCred.AccessKey:
|
case accountName == globalActiveCred.AccessKey || newGlobalAuthZPluginFn() != nil:
|
||||||
|
// For owner account and when plugin authZ is configured always set
|
||||||
|
// effective policy as `consoleAdmin`.
|
||||||
|
//
|
||||||
|
// In the latter case, we let the UI render everything, but individual
|
||||||
|
// actions would fail if not permitted by the external authZ service.
|
||||||
for _, policy := range policy.DefaultPolicies {
|
for _, policy := range policy.DefaultPolicies {
|
||||||
if policy.Name == "consoleAdmin" {
|
if policy.Name == "consoleAdmin" {
|
||||||
effectivePolicy = policy.Definition
|
effectivePolicy = policy.Definition
|
||||||
@@ -1469,8 +1474,8 @@ func (a adminAPIHandlers) AccountInfoHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
|
effectivePolicy = globalIAMSys.GetCombinedPolicy(policies...)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err = json.MarshalIndent(effectivePolicy, "", " ")
|
buf, err = json.MarshalIndent(effectivePolicy, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -1562,6 +1567,7 @@ func (a adminAPIHandlers) InfoCannedPolicy(w http.ResponseWriter, r *http.Reques
|
|||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errTooManyPolicies), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setReqInfoPolicyName(ctx, name)
|
||||||
|
|
||||||
policyDoc, err := globalIAMSys.InfoPolicy(name)
|
policyDoc, err := globalIAMSys.InfoPolicy(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1665,6 +1671,7 @@ func (a adminAPIHandlers) RemoveCannedPolicy(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
policyName := vars["name"]
|
policyName := vars["name"]
|
||||||
|
setReqInfoPolicyName(ctx, policyName)
|
||||||
|
|
||||||
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
|
if err := globalIAMSys.DeletePolicy(ctx, policyName, true); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -1697,6 +1704,13 @@ func (a adminAPIHandlers) AddCannedPolicy(w http.ResponseWriter, r *http.Request
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setReqInfoPolicyName(ctx, policyName)
|
||||||
|
|
||||||
|
// Reject policy names with commas.
|
||||||
|
if strings.Contains(policyName, ",") {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrPolicyInvalidName), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Error out if Content-Length is missing.
|
// Error out if Content-Length is missing.
|
||||||
if r.ContentLength <= 0 {
|
if r.ContentLength <= 0 {
|
||||||
@@ -1762,6 +1776,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
|||||||
policyName := vars["policyName"]
|
policyName := vars["policyName"]
|
||||||
entityName := vars["userOrGroup"]
|
entityName := vars["userOrGroup"]
|
||||||
isGroup := vars["isGroup"] == "true"
|
isGroup := vars["isGroup"] == "true"
|
||||||
|
setReqInfoPolicyName(ctx, policyName)
|
||||||
|
|
||||||
if !isGroup {
|
if !isGroup {
|
||||||
ok, _, err := globalIAMSys.IsTempUser(entityName)
|
ok, _, err := globalIAMSys.IsTempUser(entityName)
|
||||||
@@ -1847,7 +1862,7 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/polciy-entities?policy=xxx&user=xxx&group=xxx
|
// ListPolicyMappingEntities - GET /minio/admin/v3/idp/builtin/policy-entities?policy=xxx&user=xxx&group=xxx
|
||||||
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
func (a adminAPIHandlers) ListPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
@@ -1949,6 +1964,7 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht
|
|||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
setReqInfoPolicyName(ctx, strings.Join(addedOrRemoved, ","))
|
||||||
|
|
||||||
respBody := madmin.PolicyAssociationResp{
|
respBody := madmin.PolicyAssociationResp{
|
||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
@@ -1974,6 +1990,84 @@ func (a adminAPIHandlers) AttachDetachPolicyBuiltin(w http.ResponseWriter, r *ht
|
|||||||
writeSuccessResponseJSON(w, encryptedData)
|
writeSuccessResponseJSON(w, encryptedData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RevokeTokens - POST /minio/admin/v3/revoke-tokens/{userProvider}
|
||||||
|
func (a adminAPIHandlers) RevokeTokens(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
// Get current object layer instance.
|
||||||
|
objectAPI := newObjectLayerFn()
|
||||||
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||||
|
if s3Err != ErrNone {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userProvider := mux.Vars(r)["userProvider"]
|
||||||
|
|
||||||
|
user := r.Form.Get("user")
|
||||||
|
tokenRevokeType := r.Form.Get("tokenRevokeType")
|
||||||
|
fullRevoke := r.Form.Get("fullRevoke") == "true"
|
||||||
|
isTokenSelfRevoke := user == ""
|
||||||
|
if !isTokenSelfRevoke {
|
||||||
|
var err error
|
||||||
|
user, err = getUserWithProvider(ctx, userProvider, user, false)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user != "" && tokenRevokeType == "" && !fullRevoke) || (tokenRevokeType != "" && fullRevoke) {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adminPrivilege := globalIAMSys.IsAllowed(policy.Args{
|
||||||
|
AccountName: cred.AccessKey,
|
||||||
|
Groups: cred.Groups,
|
||||||
|
Action: policy.RemoveServiceAccountAdminAction,
|
||||||
|
ConditionValues: getConditionValues(r, "", cred),
|
||||||
|
IsOwner: owner,
|
||||||
|
Claims: cred.Claims,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !adminPrivilege || isTokenSelfRevoke {
|
||||||
|
parentUser := cred.AccessKey
|
||||||
|
if cred.ParentUser != "" {
|
||||||
|
parentUser = cred.ParentUser
|
||||||
|
}
|
||||||
|
if !isTokenSelfRevoke && user != parentUser {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user = parentUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infer token revoke type from the request if requestor is STS.
|
||||||
|
if isTokenSelfRevoke && tokenRevokeType == "" && !fullRevoke {
|
||||||
|
if cred.IsTemp() {
|
||||||
|
tokenRevokeType, _ = cred.Claims[tokenRevokeTypeClaim].(string)
|
||||||
|
}
|
||||||
|
if tokenRevokeType == "" {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNoTokenRevokeType), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := globalIAMSys.RevokeTokens(ctx, user, tokenRevokeType)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessNoContent(w)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
allPoliciesFile = "policies.json"
|
allPoliciesFile = "policies.json"
|
||||||
allUsersFile = "users.json"
|
allUsersFile = "users.json"
|
||||||
@@ -2003,6 +2097,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Get current object layer instance.
|
// Get current object layer instance.
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ExportIAMAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Initialize a zip writer which will provide a zipped content
|
// Initialize a zip writer which will provide a zipped content
|
||||||
@@ -2139,7 +2234,7 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
|||||||
SecretKey: acc.Credentials.SecretKey,
|
SecretKey: acc.Credentials.SecretKey,
|
||||||
Groups: acc.Credentials.Groups,
|
Groups: acc.Credentials.Groups,
|
||||||
Claims: claims,
|
Claims: claims,
|
||||||
SessionPolicy: json.RawMessage(policyJSON),
|
SessionPolicy: policyJSON,
|
||||||
Status: acc.Credentials.Status,
|
Status: acc.Credentials.Status,
|
||||||
Name: sa.Name,
|
Name: sa.Name,
|
||||||
Description: sa.Description,
|
Description: sa.Description,
|
||||||
@@ -2225,17 +2320,13 @@ func (a adminAPIHandlers) ImportIAMV2(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) {
|
func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiVer string) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
// Get current object layer instance.
|
// Validate signature, permissions and get current object layer instance.
|
||||||
objectAPI := newObjectLayerFn()
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ImportIAMAction)
|
||||||
if objectAPI == nil || globalNotificationSys == nil {
|
if objectAPI == nil || globalNotificationSys == nil {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
|
||||||
if s3Err != ErrNone {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data, err := io.ReadAll(r.Body)
|
data, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
@@ -2253,7 +2344,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
|||||||
|
|
||||||
// import policies first
|
// import policies first
|
||||||
{
|
{
|
||||||
|
|
||||||
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
|
f, err := zr.Open(pathJoin(iamAssetsDir, allPoliciesFile))
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, os.ErrNotExist):
|
case errors.Is(err, os.ErrNotExist):
|
||||||
@@ -2325,44 +2415,17 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
|
|
||||||
// Incoming access key matches parent user then we should
|
|
||||||
// reject password change requests.
|
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAddUserInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
// Check if accessKey has beginning and end space characters, this only applies to new users.
|
||||||
if !exists && hasSpaceBE(accessKey) {
|
if !exists && hasSpaceBE(accessKey) {
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAdminResourceInvalidArgument, err, allUsersFile, accessKey), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDenyOnly := false
|
|
||||||
if accessKey == cred.AccessKey {
|
|
||||||
// Check that there is no explicit deny - otherwise it's allowed
|
|
||||||
// to change one's own password.
|
|
||||||
checkDenyOnly = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !globalIAMSys.IsAllowed(policy.Args{
|
|
||||||
AccountName: cred.AccessKey,
|
|
||||||
Groups: cred.Groups,
|
|
||||||
Action: policy.CreateUserAdminAction,
|
|
||||||
ConditionValues: getConditionValues(r, "", cred),
|
|
||||||
IsOwner: owner,
|
|
||||||
Claims: cred.Claims,
|
|
||||||
DenyOnly: checkDenyOnly,
|
|
||||||
}) {
|
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allUsersFile, accessKey), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
|
if _, err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
|
||||||
failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err})
|
failed.Users = append(failed.Users, madmin.IAMErrEntity{Name: accessKey, Error: err})
|
||||||
} else {
|
} else {
|
||||||
added.Users = append(added.Users, accessKey)
|
added.Users = append(added.Users, accessKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2456,17 +2519,6 @@ func (a adminAPIHandlers) importIAM(w http.ResponseWriter, r *http.Request, apiV
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminResourceInvalidArgument), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !globalIAMSys.IsAllowed(policy.Args{
|
|
||||||
AccountName: cred.AccessKey,
|
|
||||||
Groups: cred.Groups,
|
|
||||||
Action: policy.CreateServiceAccountAdminAction,
|
|
||||||
ConditionValues: getConditionValues(r, "", cred),
|
|
||||||
IsOwner: owner,
|
|
||||||
Claims: cred.Claims,
|
|
||||||
}) {
|
|
||||||
writeErrorResponseJSON(ctx, w, importErrorWithAPIErr(ctx, ErrAccessDenied, err, allSvcAcctsFile, user), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updateReq := true
|
updateReq := true
|
||||||
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
|
_, _, err = globalIAMSys.GetServiceAccount(ctx, svcAcctReq.AccessKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2686,7 +2738,7 @@ func addExpirationToCondValues(exp *time.Time, condValues map[string][]string) e
|
|||||||
if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) {
|
if exp == nil || exp.IsZero() || exp.Equal(timeSentinel) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dur := exp.Sub(time.Now())
|
dur := time.Until(*exp)
|
||||||
if dur <= 0 {
|
if dur <= 0 {
|
||||||
return errors.New("unsupported expiration time")
|
return errors.New("unsupported expiration time")
|
||||||
}
|
}
|
||||||
@@ -2795,3 +2847,10 @@ func commonAddServiceAccount(r *http.Request, ldap bool) (context.Context, auth.
|
|||||||
|
|
||||||
return ctx, cred, opts, createReq, targetUser, APIError{}
|
return ctx, cred, opts, createReq, targetUser, APIError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setReqInfoPolicyName will set the given policyName as a tag on the context's request info,
|
||||||
|
// so that it appears in audit logs.
|
||||||
|
func setReqInfoPolicyName(ctx context.Context, policyName string) {
|
||||||
|
reqInfo := logger.GetReqInfo(ctx)
|
||||||
|
reqInfo.SetTags("policyName", policyName)
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func (s *TestSuiteIAM) SetUpSuite(c *check) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *TestSuiteIAM) RestartIAMSuite(c *check) {
|
func (s *TestSuiteIAM) RestartIAMSuite(c *check) {
|
||||||
s.TestSuiteCommon.RestartTestServer(c)
|
s.RestartTestServer(c)
|
||||||
|
|
||||||
s.iamSetup(c)
|
s.iamSetup(c)
|
||||||
}
|
}
|
||||||
@@ -716,6 +716,12 @@ func (s *TestSuiteIAM) TestCannedPolicies(c *check) {
|
|||||||
c.Fatalf("policy info err: %v", err)
|
c.Fatalf("policy info err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that policy with comma is rejected.
|
||||||
|
err = s.adm.AddCannedPolicy(ctx, "invalid,policy", policyBytes)
|
||||||
|
if err == nil {
|
||||||
|
c.Fatalf("invalid policy created successfully")
|
||||||
|
}
|
||||||
|
|
||||||
infoStr := string(info)
|
infoStr := string(info)
|
||||||
if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
|
if !strings.Contains(infoStr, `"s3:PutObject"`) || !strings.Contains(infoStr, ":"+bucket+"/") {
|
||||||
c.Fatalf("policy contains unexpected content!")
|
c.Fatalf("policy contains unexpected content!")
|
||||||
|
|||||||
+80
-53
@@ -49,6 +49,7 @@ import (
|
|||||||
"github.com/klauspost/compress/zip"
|
"github.com/klauspost/compress/zip"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/madmin-go/v3/estream"
|
"github.com/minio/madmin-go/v3/estream"
|
||||||
|
"github.com/minio/madmin-go/v3/logger/log"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/dsync"
|
"github.com/minio/minio/internal/dsync"
|
||||||
@@ -59,7 +60,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/kms"
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/mux"
|
"github.com/minio/mux"
|
||||||
"github.com/minio/pkg/v3/logger/message/log"
|
|
||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
"github.com/minio/pkg/v3/policy"
|
"github.com/minio/pkg/v3/policy"
|
||||||
"github.com/secure-io/sio-go"
|
"github.com/secure-io/sio-go"
|
||||||
@@ -93,11 +93,18 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalInplaceUpdateDisabled || currentReleaseTime.IsZero() {
|
if globalInplaceUpdateDisabled {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrMethodNotAllowed), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if currentReleaseTime.IsZero() || currentReleaseTime.Equal(timeSentinel) {
|
||||||
|
apiErr := errorCodes.ToAPIErr(ErrMethodNotAllowed)
|
||||||
|
apiErr.Description = fmt.Sprintf("unable to perform in-place update, release time is unrecognized: %s", currentReleaseTime)
|
||||||
|
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
updateURL := vars["updateURL"]
|
updateURL := vars["updateURL"]
|
||||||
dryRun := r.Form.Get("dry-run") == "true"
|
dryRun := r.Form.Get("dry-run") == "true"
|
||||||
@@ -110,6 +117,11 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local := globalLocalNodeName
|
||||||
|
if local == "" {
|
||||||
|
local = "127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
u, err := url.Parse(updateURL)
|
u, err := url.Parse(updateURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@@ -128,6 +140,39 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateStatus := madmin.ServerUpdateStatusV2{
|
||||||
|
DryRun: dryRun,
|
||||||
|
Results: make([]madmin.ServerPeerUpdateStatus, 0, len(globalNotificationSys.allPeerClients)),
|
||||||
|
}
|
||||||
|
peerResults := make(map[string]madmin.ServerPeerUpdateStatus, len(globalNotificationSys.allPeerClients))
|
||||||
|
failedClients := make(map[int]bool, len(globalNotificationSys.allPeerClients))
|
||||||
|
|
||||||
|
if lrTime.Sub(currentReleaseTime) <= 0 {
|
||||||
|
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
|
||||||
|
Host: local,
|
||||||
|
Err: fmt.Sprintf("server is running the latest version: %s", Version),
|
||||||
|
CurrentVersion: Version,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, client := range globalNotificationSys.peerClients {
|
||||||
|
updateStatus.Results = append(updateStatus.Results, madmin.ServerPeerUpdateStatus{
|
||||||
|
Host: client.String(),
|
||||||
|
Err: fmt.Sprintf("server is running the latest version: %s", Version),
|
||||||
|
CurrentVersion: Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal API response
|
||||||
|
jsonBytes, err := json.Marshal(updateStatus)
|
||||||
|
if err != nil {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSuccessResponseJSON(w, jsonBytes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
u.Path = path.Dir(u.Path) + SlashSeparator + releaseInfo
|
u.Path = path.Dir(u.Path) + SlashSeparator + releaseInfo
|
||||||
// Download Binary Once
|
// Download Binary Once
|
||||||
binC, bin, err := downloadBinary(u, mode)
|
binC, bin, err := downloadBinary(u, mode)
|
||||||
@@ -137,16 +182,6 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatus := madmin.ServerUpdateStatusV2{DryRun: dryRun}
|
|
||||||
peerResults := make(map[string]madmin.ServerPeerUpdateStatus)
|
|
||||||
|
|
||||||
local := globalLocalNodeName
|
|
||||||
if local == "" {
|
|
||||||
local = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
failedClients := make(map[int]struct{})
|
|
||||||
|
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
// Push binary to other servers
|
// Push binary to other servers
|
||||||
for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
|
for idx, nerr := range globalNotificationSys.VerifyBinary(ctx, u, sha256Sum, releaseInfo, binC) {
|
||||||
@@ -156,7 +191,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
Err: nerr.Err.Error(),
|
Err: nerr.Err.Error(),
|
||||||
CurrentVersion: Version,
|
CurrentVersion: Version,
|
||||||
}
|
}
|
||||||
failedClients[idx] = struct{}{}
|
failedClients[idx] = true
|
||||||
} else {
|
} else {
|
||||||
peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
|
peerResults[nerr.Host.String()] = madmin.ServerPeerUpdateStatus{
|
||||||
Host: nerr.Host.String(),
|
Host: nerr.Host.String(),
|
||||||
@@ -167,25 +202,17 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if lrTime.Sub(currentReleaseTime) > 0 {
|
if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
|
||||||
if err = verifyBinary(u, sha256Sum, releaseInfo, mode, bytes.NewReader(bin)); err != nil {
|
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
Host: local,
|
||||||
Host: local,
|
Err: err.Error(),
|
||||||
Err: err.Error(),
|
CurrentVersion: Version,
|
||||||
CurrentVersion: Version,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
|
||||||
Host: local,
|
|
||||||
CurrentVersion: Version,
|
|
||||||
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
peerResults[local] = madmin.ServerPeerUpdateStatus{
|
||||||
Host: local,
|
Host: local,
|
||||||
Err: fmt.Sprintf("server is already running the latest version: %s", Version),
|
|
||||||
CurrentVersion: Version,
|
CurrentVersion: Version,
|
||||||
|
UpdatedVersion: lrTime.Format(MinioReleaseTagTimeLayout),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +220,7 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
||||||
for idx, client := range globalNotificationSys.peerClients {
|
for idx, client := range globalNotificationSys.peerClients {
|
||||||
_, ok := failedClients[idx]
|
if failedClients[idx] {
|
||||||
if ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
client := client
|
client := client
|
||||||
@@ -240,14 +266,14 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
startTime := time.Now().Add(restartUpdateDelay)
|
startTime := time.Now().Add(restartUpdateDelay)
|
||||||
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
ng := WithNPeers(len(globalNotificationSys.peerClients))
|
||||||
for idx, client := range globalNotificationSys.peerClients {
|
for idx, client := range globalNotificationSys.peerClients {
|
||||||
_, ok := failedClients[idx]
|
if failedClients[idx] {
|
||||||
if ok {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
client := client
|
client := client
|
||||||
ng.Go(ctx, func() error {
|
ng.Go(ctx, func() error {
|
||||||
prs, ok := peerResults[client.String()]
|
prs, ok := peerResults[client.String()]
|
||||||
if ok && prs.CurrentVersion != prs.UpdatedVersion && prs.UpdatedVersion != "" {
|
// We restart only on success, not for any failures.
|
||||||
|
if ok && prs.Err == "" {
|
||||||
return client.SignalService(serviceRestart, "", dryRun, &startTime)
|
return client.SignalService(serviceRestart, "", dryRun, &startTime)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -284,7 +310,9 @@ func (a adminAPIHandlers) ServerUpdateV2Handler(w http.ResponseWriter, r *http.R
|
|||||||
writeSuccessResponseJSON(w, jsonBytes)
|
writeSuccessResponseJSON(w, jsonBytes)
|
||||||
|
|
||||||
if !dryRun {
|
if !dryRun {
|
||||||
if lrTime.Sub(currentReleaseTime) > 0 {
|
prs, ok := peerResults[local]
|
||||||
|
// We restart only on success, not for any failures.
|
||||||
|
if ok && prs.Err == "" {
|
||||||
globalServiceSignalCh <- serviceRestart
|
globalServiceSignalCh <- serviceRestart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -801,7 +829,7 @@ func (a adminAPIHandlers) MetricsHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Flush before waiting for next...
|
// Flush before waiting for next...
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
@@ -1292,7 +1320,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Analyze the heal token and route the request accordingly
|
// Analyze the heal token and route the request accordingly
|
||||||
token, success := proxyRequestByToken(ctx, w, r, hip.clientToken)
|
token, _, success := proxyRequestByToken(ctx, w, r, hip.clientToken, false)
|
||||||
if success {
|
if success {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1331,7 +1359,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case hr := <-respCh:
|
case hr := <-respCh:
|
||||||
switch hr.apiErr {
|
switch hr.apiErr {
|
||||||
case noError:
|
case noError:
|
||||||
@@ -1339,7 +1367,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if _, err := w.Write(hr.respBytes); err != nil {
|
if _, err := w.Write(hr.respBytes); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
} else {
|
} else {
|
||||||
writeSuccessResponseJSON(w, hr.respBytes)
|
writeSuccessResponseJSON(w, hr.respBytes)
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1394,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if _, err := w.Write(errorRespJSON); err != nil {
|
if _, err := w.Write(errorRespJSON); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
break forLoop
|
break forLoop
|
||||||
}
|
}
|
||||||
@@ -1379,7 +1407,7 @@ func (a adminAPIHandlers) HealHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 {
|
if exists && !nh.hasEnded() && len(nh.currentStatus.Items) > 0 {
|
||||||
clientToken := nh.clientToken
|
clientToken := nh.clientToken
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
clientToken = fmt.Sprintf("%s:%d", nh.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
clientToken = fmt.Sprintf("%s%s%d", nh.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||||
}
|
}
|
||||||
b, err := json.Marshal(madmin.HealStartSuccess{
|
b, err := json.Marshal(madmin.HealStartSuccess{
|
||||||
ClientToken: clientToken,
|
ClientToken: clientToken,
|
||||||
@@ -1583,7 +1611,6 @@ func (a adminAPIHandlers) ClientDevNull(w http.ResponseWriter, r *http.Request)
|
|||||||
if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte {
|
if err != nil || ctx.Err() != nil || totalRx > 100*humanize.GiByte {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -1812,7 +1839,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case result, ok := <-ch:
|
case result, ok := <-ch:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -1821,7 +1848,7 @@ func (a adminAPIHandlers) ObjectSpeedTestHandler(w http.ResponseWriter, r *http.
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
prevResult = result
|
prevResult = result
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1930,7 +1957,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
|||||||
if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil {
|
if err := enc.Encode(madmin.DriveSpeedTestResult{}); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case result, ok := <-ch:
|
case result, ok := <-ch:
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -1938,7 +1965,7 @@ func (a adminAPIHandlers) DriveSpeedtestHandler(w http.ResponseWriter, r *http.R
|
|||||||
if err := enc.Encode(result); err != nil {
|
if err := enc.Encode(result); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2055,7 +2082,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
grid.PutByteBuffer(entry)
|
grid.PutByteBuffer(entry)
|
||||||
if len(traceCh) == 0 {
|
if len(traceCh) == 0 {
|
||||||
// Flush if nothing is queued
|
// Flush if nothing is queued
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
case <-keepAliveTicker.C:
|
case <-keepAliveTicker.C:
|
||||||
if len(traceCh) > 0 {
|
if len(traceCh) > 0 {
|
||||||
@@ -2064,7 +2091,7 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2156,7 +2183,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
grid.PutByteBuffer(log)
|
grid.PutByteBuffer(log)
|
||||||
if len(logCh) == 0 {
|
if len(logCh) == 0 {
|
||||||
// Flush if nothing is queued
|
// Flush if nothing is queued
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
case <-keepAliveTicker.C:
|
case <-keepAliveTicker.C:
|
||||||
if len(logCh) > 0 {
|
if len(logCh) > 0 {
|
||||||
@@ -2165,7 +2192,7 @@ func (a adminAPIHandlers) ConsoleLogHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2649,7 +2676,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
|||||||
// disk metrics are already included under drive info of each server
|
// disk metrics are already included under drive info of each server
|
||||||
getRealtimeMetrics := func() *madmin.RealtimeMetrics {
|
getRealtimeMetrics := func() *madmin.RealtimeMetrics {
|
||||||
var m madmin.RealtimeMetrics
|
var m madmin.RealtimeMetrics
|
||||||
var types madmin.MetricType = madmin.MetricsAll &^ madmin.MetricsDisk
|
types := madmin.MetricsAll &^ madmin.MetricsDisk
|
||||||
mLocal := collectLocalMetrics(types, collectMetricsOpts{})
|
mLocal := collectLocalMetrics(types, collectMetricsOpts{})
|
||||||
m.Merge(&mLocal)
|
m.Merge(&mLocal)
|
||||||
cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
|
cctx, cancel := context.WithTimeout(healthCtx, time.Second/2)
|
||||||
@@ -2693,7 +2720,7 @@ func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *ur
|
|||||||
poolsArgs := re.ReplaceAllString(cmdLine, `$3`)
|
poolsArgs := re.ReplaceAllString(cmdLine, `$3`)
|
||||||
var anonPools []string
|
var anonPools []string
|
||||||
|
|
||||||
if !(strings.Contains(poolsArgs, "{") && strings.Contains(poolsArgs, "}")) {
|
if !strings.Contains(poolsArgs, "{") || !strings.Contains(poolsArgs, "}") {
|
||||||
// No ellipses pattern. Anonymize host name from every pool arg
|
// No ellipses pattern. Anonymize host name from every pool arg
|
||||||
pools := strings.Fields(poolsArgs)
|
pools := strings.Fields(poolsArgs)
|
||||||
anonPools = make([]string, len(pools))
|
anonPools = make([]string, len(pools))
|
||||||
@@ -2935,13 +2962,13 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
if len(healthInfoCh) == 0 {
|
if len(healthInfoCh) == 0 {
|
||||||
// Flush if nothing is queued
|
// Flush if nothing is queued
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
}
|
}
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if _, err := w.Write([]byte(" ")); err != nil {
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
xhttp.Flush(w)
|
||||||
case <-healthCtx.Done():
|
case <-healthCtx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3393,7 +3420,7 @@ func (a adminAPIHandlers) InspectDataHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save the format.json as part of inspect by default
|
// save the format.json as part of inspect by default
|
||||||
if !(volume == minioMetaBucket && file == formatConfigFile) {
|
if volume != minioMetaBucket || file != formatConfigFile {
|
||||||
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
|
err = o.GetRawData(ctx, minioMetaBucket, formatConfigFile, rawDataFn)
|
||||||
}
|
}
|
||||||
if !errors.Is(err, errFileNotFound) {
|
if !errors.Is(err, errFileNotFound) {
|
||||||
|
|||||||
+13
-21
@@ -260,7 +260,7 @@ func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
|
|||||||
} else {
|
} else {
|
||||||
clientToken := he.clientToken
|
clientToken := he.clientToken
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
clientToken = fmt.Sprintf("%s:%d", he.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
clientToken = fmt.Sprintf("%s%s%d", he.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||||
}
|
}
|
||||||
|
|
||||||
hsp = madmin.HealStopSuccess{
|
hsp = madmin.HealStopSuccess{
|
||||||
@@ -331,7 +331,7 @@ func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLay
|
|||||||
|
|
||||||
clientToken := h.clientToken
|
clientToken := h.clientToken
|
||||||
if globalIsDistErasure {
|
if globalIsDistErasure {
|
||||||
clientToken = fmt.Sprintf("%s:%d", h.clientToken, GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
clientToken = fmt.Sprintf("%s%s%d", h.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.clientToken == bgHealingUUID {
|
if h.clientToken == bgHealingUUID {
|
||||||
@@ -806,18 +806,20 @@ func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
|||||||
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
func (h *healSequence) healItems(objAPI ObjectLayer) error {
|
||||||
if h.clientToken == bgHealingUUID {
|
if h.clientToken == bgHealingUUID {
|
||||||
// For background heal do nothing.
|
// For background heal do nothing.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.healDiskMeta(objAPI); err != nil {
|
if h.bucket == "" { // heal internal meta only during a site-wide heal
|
||||||
return err
|
if err := h.healDiskMeta(objAPI); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heal buckets and objects
|
// Heal buckets and objects
|
||||||
return h.healBuckets(objAPI, bucketsOnly)
|
return h.healBuckets(objAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// traverseAndHeal - traverses on-disk data and performs healing
|
// traverseAndHeal - traverses on-disk data and performs healing
|
||||||
@@ -828,8 +830,7 @@ func (h *healSequence) healItems(objAPI ObjectLayer, bucketsOnly bool) error {
|
|||||||
// has to wait until a safe point is reached, such as between scanning
|
// has to wait until a safe point is reached, such as between scanning
|
||||||
// two objects.
|
// two objects.
|
||||||
func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
|
func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
|
||||||
bucketsOnly := false // Heals buckets and objects also.
|
h.traverseAndHealDoneCh <- h.healItems(objAPI)
|
||||||
h.traverseAndHealDoneCh <- h.healItems(objAPI, bucketsOnly)
|
|
||||||
xioutil.SafeClose(h.traverseAndHealDoneCh)
|
xioutil.SafeClose(h.traverseAndHealDoneCh)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,6 +841,7 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
|||||||
// NOTE: Healing on meta is run regardless
|
// NOTE: Healing on meta is run regardless
|
||||||
// of any bucket being selected, this is to ensure that
|
// of any bucket being selected, this is to ensure that
|
||||||
// meta are always upto date and correct.
|
// meta are always upto date and correct.
|
||||||
|
h.settings.Recursive = true
|
||||||
return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
|
return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
|
||||||
if h.isQuitting() {
|
if h.isQuitting() {
|
||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
@@ -856,14 +858,14 @@ func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) f
|
|||||||
}
|
}
|
||||||
|
|
||||||
// healBuckets - check for all buckets heal or just particular bucket.
|
// healBuckets - check for all buckets heal or just particular bucket.
|
||||||
func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
func (h *healSequence) healBuckets(objAPI ObjectLayer) error {
|
||||||
if h.isQuitting() {
|
if h.isQuitting() {
|
||||||
return errHealStopSignalled
|
return errHealStopSignalled
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. If a bucket was specified, heal only the bucket.
|
// 1. If a bucket was specified, heal only the bucket.
|
||||||
if h.bucket != "" {
|
if h.bucket != "" {
|
||||||
return h.healBucket(objAPI, h.bucket, bucketsOnly)
|
return h.healBucket(objAPI, h.bucket, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
|
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
|
||||||
@@ -877,7 +879,7 @@ func (h *healSequence) healBuckets(objAPI ObjectLayer, bucketsOnly bool) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
if err = h.healBucket(objAPI, bucket.Name, bucketsOnly); err != nil {
|
if err = h.healBucket(objAPI, bucket.Name, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -895,16 +897,6 @@ func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !h.settings.Recursive {
|
|
||||||
if h.object != "" {
|
|
||||||
if err := h.healObject(bucket, h.object, "", h.settings.ScanMode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
||||||
return errFnHealFromAPIErr(h.ctx, err)
|
return errFnHealFromAPIErr(h.ctx, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,6 +424,9 @@ func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
|||||||
// -- Health API --
|
// -- Health API --
|
||||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
|
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
|
||||||
HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
|
HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
|
||||||
|
|
||||||
|
// STS Revocation
|
||||||
|
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/revoke-tokens/{userProvider}").HandlerFunc(adminMiddleware(adminAPI.RevokeTokens))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If none of the routes match add default error handler routes
|
// If none of the routes match add default error handler routes
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type DeletedObject struct {
|
|||||||
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
||||||
// MinIO extensions to support delete marker replication
|
// MinIO extensions to support delete marker replication
|
||||||
ReplicationState ReplicationState `xml:"-"`
|
ReplicationState ReplicationState `xml:"-"`
|
||||||
|
|
||||||
|
found bool // the object was found during deletion
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
||||||
@@ -42,10 +44,10 @@ type DeleteMarkerMTime struct {
|
|||||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||||
// empty string otherwise
|
// empty string otherwise
|
||||||
func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||||
if t.Time.IsZero() {
|
if t.IsZero() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e.EncodeElement(t.Time.Format(time.RFC3339), startElement)
|
return e.EncodeElement(t.Format(time.RFC3339), startElement)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectV object version key/versionId
|
// ObjectV object version key/versionId
|
||||||
|
|||||||
+16
-2
@@ -213,6 +213,8 @@ const (
|
|||||||
ErrPolicyAlreadyAttached
|
ErrPolicyAlreadyAttached
|
||||||
ErrPolicyNotAttached
|
ErrPolicyNotAttached
|
||||||
ErrExcessData
|
ErrExcessData
|
||||||
|
ErrPolicyInvalidName
|
||||||
|
ErrNoTokenRevokeType
|
||||||
// Add new error codes here.
|
// Add new error codes here.
|
||||||
|
|
||||||
// SSE-S3/SSE-KMS related API errors
|
// SSE-S3/SSE-KMS related API errors
|
||||||
@@ -561,6 +563,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "More data provided than indicated content length",
|
Description: "More data provided than indicated content length",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
|
ErrPolicyInvalidName: {
|
||||||
|
Code: "PolicyInvalidName",
|
||||||
|
Description: "Policy name may not contain comma",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
ErrPolicyTooLarge: {
|
ErrPolicyTooLarge: {
|
||||||
Code: "PolicyTooLarge",
|
Code: "PolicyTooLarge",
|
||||||
Description: "Policy exceeds the maximum allowed document size.",
|
Description: "Policy exceeds the maximum allowed document size.",
|
||||||
@@ -623,7 +630,7 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrMissingContentMD5: {
|
ErrMissingContentMD5: {
|
||||||
Code: "MissingContentMD5",
|
Code: "MissingContentMD5",
|
||||||
Description: "Missing required header for this request: Content-Md5.",
|
Description: "Missing or invalid required header for this request: Content-Md5 or Amz-Content-Checksum",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrMissingSecurityHeader: {
|
ErrMissingSecurityHeader: {
|
||||||
@@ -978,7 +985,7 @@ var errorCodes = errorCodeMap{
|
|||||||
},
|
},
|
||||||
ErrReplicationNoExistingObjects: {
|
ErrReplicationNoExistingObjects: {
|
||||||
Code: "XMinioReplicationNoExistingObjects",
|
Code: "XMinioReplicationNoExistingObjects",
|
||||||
Description: "No matching ExistingsObjects rule enabled",
|
Description: "No matching ExistingObjects rule enabled",
|
||||||
HTTPStatusCode: http.StatusBadRequest,
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
},
|
},
|
||||||
ErrRemoteTargetDenyAddError: {
|
ErrRemoteTargetDenyAddError: {
|
||||||
@@ -1258,6 +1265,11 @@ var errorCodes = errorCodeMap{
|
|||||||
Description: "The security token included in the request is invalid",
|
Description: "The security token included in the request is invalid",
|
||||||
HTTPStatusCode: http.StatusForbidden,
|
HTTPStatusCode: http.StatusForbidden,
|
||||||
},
|
},
|
||||||
|
ErrNoTokenRevokeType: {
|
||||||
|
Code: "InvalidArgument",
|
||||||
|
Description: "No token revoke type specified and one could not be inferred from the request",
|
||||||
|
HTTPStatusCode: http.StatusBadRequest,
|
||||||
|
},
|
||||||
|
|
||||||
// S3 extensions.
|
// S3 extensions.
|
||||||
ErrContentSHA256Mismatch: {
|
ErrContentSHA256Mismatch: {
|
||||||
@@ -2248,6 +2260,8 @@ func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) {
|
|||||||
apiErr = ErrServerNotInitialized
|
apiErr = ErrServerNotInitialized
|
||||||
case errBucketMetadataNotInitialized:
|
case errBucketMetadataNotInitialized:
|
||||||
apiErr = ErrBucketMetadataNotInitialized
|
apiErr = ErrBucketMetadataNotInitialized
|
||||||
|
case hash.ErrInvalidChecksum:
|
||||||
|
apiErr = ErrInvalidChecksum
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compression errors
|
// Compression errors
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func TestNewRequestID(t *testing.T) {
|
|||||||
e = char
|
e = char
|
||||||
|
|
||||||
// Ensure that it is alphanumeric, in this case, between 0-9 and A-Z.
|
// Ensure that it is alphanumeric, in this case, between 0-9 and A-Z.
|
||||||
if !(('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')) {
|
isAlnum := ('0' <= e && e <= '9') || ('A' <= e && e <= 'Z')
|
||||||
|
if !isAlnum {
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-18
@@ -166,10 +166,11 @@ type Part struct {
|
|||||||
Size int64
|
Size int64
|
||||||
|
|
||||||
// Checksum values
|
// Checksum values
|
||||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||||
|
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPartsResponse - format for list parts response.
|
// ListPartsResponse - format for list parts response.
|
||||||
@@ -192,6 +193,8 @@ type ListPartsResponse struct {
|
|||||||
IsTruncated bool
|
IsTruncated bool
|
||||||
|
|
||||||
ChecksumAlgorithm string
|
ChecksumAlgorithm string
|
||||||
|
ChecksumType string
|
||||||
|
|
||||||
// List of parts.
|
// List of parts.
|
||||||
Parts []Part `xml:"Part"`
|
Parts []Part `xml:"Part"`
|
||||||
}
|
}
|
||||||
@@ -413,10 +416,11 @@ type CompleteMultipartUploadResponse struct {
|
|||||||
Key string
|
Key string
|
||||||
ETag string
|
ETag string
|
||||||
|
|
||||||
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
ChecksumCRC32 string `xml:"ChecksumCRC32,omitempty"`
|
||||||
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
ChecksumCRC32C string `xml:"ChecksumCRC32C,omitempty"`
|
||||||
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
ChecksumSHA1 string `xml:"ChecksumSHA1,omitempty"`
|
||||||
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
ChecksumSHA256 string `xml:"ChecksumSHA256,omitempty"`
|
||||||
|
ChecksumCRC64NVME string `xml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteError structure.
|
// DeleteError structure.
|
||||||
@@ -516,7 +520,6 @@ func cleanReservedKeys(metadata map[string]string) map[string]string {
|
|||||||
}
|
}
|
||||||
case crypto.SSEC:
|
case crypto.SSEC:
|
||||||
m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
|
m[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var toRemove []string
|
var toRemove []string
|
||||||
@@ -593,8 +596,6 @@ func generateListVersionsResponse(ctx context.Context, bucket, prefix, marker, v
|
|||||||
for k, v := range cleanReservedKeys(object.UserDefined) {
|
for k, v := range cleanReservedKeys(object.UserDefined) {
|
||||||
content.UserMetadata.Set(k, v)
|
content.UserMetadata.Set(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
|
|
||||||
content.Internal = &ObjectInternalInfo{
|
content.Internal = &ObjectInternalInfo{
|
||||||
K: object.DataBlocks,
|
K: object.DataBlocks,
|
||||||
M: object.ParityBlocks,
|
M: object.ParityBlocks,
|
||||||
@@ -729,7 +730,6 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n
|
|||||||
for k, v := range cleanReservedKeys(object.UserDefined) {
|
for k, v := range cleanReservedKeys(object.UserDefined) {
|
||||||
content.UserMetadata.Set(k, v)
|
content.UserMetadata.Set(k, v)
|
||||||
}
|
}
|
||||||
content.UserMetadata.Set("expires", object.Expires.Format(http.TimeFormat))
|
|
||||||
content.Internal = &ObjectInternalInfo{
|
content.Internal = &ObjectInternalInfo{
|
||||||
K: object.DataBlocks,
|
K: object.DataBlocks,
|
||||||
M: object.ParityBlocks,
|
M: object.ParityBlocks,
|
||||||
@@ -790,17 +790,18 @@ func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) Initi
|
|||||||
|
|
||||||
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag.
|
||||||
func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
|
func generateCompleteMultipartUploadResponse(bucket, key, location string, oi ObjectInfo, h http.Header) CompleteMultipartUploadResponse {
|
||||||
cs := oi.decryptChecksums(0, h)
|
cs, _ := oi.decryptChecksums(0, h)
|
||||||
c := CompleteMultipartUploadResponse{
|
c := CompleteMultipartUploadResponse{
|
||||||
Location: location,
|
Location: location,
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
// AWS S3 quotes the ETag in XML, make sure we are compatible here.
|
// AWS S3 quotes the ETag in XML, make sure we are compatible here.
|
||||||
ETag: "\"" + oi.ETag + "\"",
|
ETag: "\"" + oi.ETag + "\"",
|
||||||
ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
|
ChecksumSHA1: cs[hash.ChecksumSHA1.String()],
|
||||||
ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
|
ChecksumSHA256: cs[hash.ChecksumSHA256.String()],
|
||||||
ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
|
ChecksumCRC32: cs[hash.ChecksumCRC32.String()],
|
||||||
ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
|
ChecksumCRC32C: cs[hash.ChecksumCRC32C.String()],
|
||||||
|
ChecksumCRC64NVME: cs[hash.ChecksumCRC64NVME.String()],
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -828,6 +829,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
|||||||
listPartsResponse.IsTruncated = partsInfo.IsTruncated
|
listPartsResponse.IsTruncated = partsInfo.IsTruncated
|
||||||
listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
|
listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker
|
||||||
listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm
|
listPartsResponse.ChecksumAlgorithm = partsInfo.ChecksumAlgorithm
|
||||||
|
listPartsResponse.ChecksumType = partsInfo.ChecksumType
|
||||||
|
|
||||||
listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
|
listPartsResponse.Parts = make([]Part, len(partsInfo.Parts))
|
||||||
for index, part := range partsInfo.Parts {
|
for index, part := range partsInfo.Parts {
|
||||||
@@ -840,6 +842,7 @@ func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) Lis
|
|||||||
newPart.ChecksumCRC32C = part.ChecksumCRC32C
|
newPart.ChecksumCRC32C = part.ChecksumCRC32C
|
||||||
newPart.ChecksumSHA1 = part.ChecksumSHA1
|
newPart.ChecksumSHA1 = part.ChecksumSHA1
|
||||||
newPart.ChecksumSHA256 = part.ChecksumSHA256
|
newPart.ChecksumSHA256 = part.ChecksumSHA256
|
||||||
|
newPart.ChecksumCRC64NVME = part.ChecksumCRC64NVME
|
||||||
listPartsResponse.Parts[index] = newPart
|
listPartsResponse.Parts[index] = newPart
|
||||||
}
|
}
|
||||||
return listPartsResponse
|
return listPartsResponse
|
||||||
|
|||||||
+2
-2
@@ -227,13 +227,13 @@ func s3APIMiddleware(f http.HandlerFunc, flags ...s3HFlag) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip wrapping with the gzip middleware if specified.
|
// Skip wrapping with the gzip middleware if specified.
|
||||||
var gzippedHandler http.HandlerFunc = tracedHandler
|
gzippedHandler := tracedHandler
|
||||||
if !handlerFlags.has(noGZS3HFlag) {
|
if !handlerFlags.has(noGZS3HFlag) {
|
||||||
gzippedHandler = gzipHandler(gzippedHandler)
|
gzippedHandler = gzipHandler(gzippedHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip wrapping with throttling middleware if specified.
|
// Skip wrapping with throttling middleware if specified.
|
||||||
var throttledHandler http.HandlerFunc = gzippedHandler
|
throttledHandler := gzippedHandler
|
||||||
if !handlerFlags.has(noThrottleS3HFlag) {
|
if !handlerFlags.has(noThrottleS3HFlag) {
|
||||||
throttledHandler = maxClients(throttledHandler)
|
throttledHandler = maxClients(throttledHandler)
|
||||||
}
|
}
|
||||||
|
|||||||
+201
-199
File diff suppressed because one or more lines are too long
+9
-30
@@ -96,7 +96,7 @@ func isRequestSignStreamingTrailerV4(r *http.Request) bool {
|
|||||||
// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
|
// Verify if the request has AWS Streaming Signature Version '4', with unsigned content and trailer.
|
||||||
func isRequestUnsignedTrailerV4(r *http.Request) bool {
|
func isRequestUnsignedTrailerV4(r *http.Request) bool {
|
||||||
return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
|
return r.Header.Get(xhttp.AmzContentSha256) == unsignedPayloadTrailer &&
|
||||||
r.Method == http.MethodPut && strings.Contains(r.Header.Get(xhttp.ContentEncoding), streamingContentEncoding)
|
r.Method == http.MethodPut
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization type.
|
// Authorization type.
|
||||||
@@ -162,7 +162,6 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
|
|||||||
s3Err := ErrAccessDenied
|
s3Err := ErrAccessDenied
|
||||||
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
if _, ok := r.Header[xhttp.AmzContentSha256]; ok &&
|
||||||
getRequestAuthType(r) == authTypeSigned {
|
getRequestAuthType(r) == authTypeSigned {
|
||||||
|
|
||||||
// Get credential information from the request.
|
// Get credential information from the request.
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
@@ -364,7 +363,7 @@ func authenticateRequest(ctx context.Context, r *http.Request, action policy.Act
|
|||||||
var cred auth.Credentials
|
var cred auth.Credentials
|
||||||
var owner bool
|
var owner bool
|
||||||
switch getRequestAuthType(r) {
|
switch getRequestAuthType(r) {
|
||||||
case authTypeUnknown, authTypeStreamingSigned:
|
case authTypeUnknown, authTypeStreamingSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
|
||||||
return ErrSignatureVersionNotSupported
|
return ErrSignatureVersionNotSupported
|
||||||
case authTypePresignedV2, authTypeSignedV2:
|
case authTypePresignedV2, authTypeSignedV2:
|
||||||
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
||||||
@@ -675,32 +674,6 @@ func setAuthMiddleware(h http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, APIErrorCode) {
|
|
||||||
var cred auth.Credentials
|
|
||||||
var owner bool
|
|
||||||
var s3Err APIErrorCode
|
|
||||||
switch atype {
|
|
||||||
case authTypeUnknown, authTypeStreamingSigned:
|
|
||||||
return cred, owner, ErrSignatureVersionNotSupported
|
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
|
||||||
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
|
|
||||||
return cred, owner, s3Err
|
|
||||||
}
|
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
|
||||||
case authTypePresigned, authTypeSigned:
|
|
||||||
region := globalSite.Region()
|
|
||||||
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
|
|
||||||
return cred, owner, s3Err
|
|
||||||
}
|
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
|
||||||
}
|
|
||||||
if s3Err != ErrNone {
|
|
||||||
return cred, owner, s3Err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cred, owner, ErrNone
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool) (s3Err APIErrorCode) {
|
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool) (s3Err APIErrorCode) {
|
||||||
var retSet bool
|
var retSet bool
|
||||||
if cred.AccessKey == "" {
|
if cred.AccessKey == "" {
|
||||||
@@ -755,8 +728,14 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
|
|||||||
return ErrSignatureVersionNotSupported
|
return ErrSignatureVersionNotSupported
|
||||||
case authTypeSignedV2, authTypePresignedV2:
|
case authTypeSignedV2, authTypePresignedV2:
|
||||||
cred, owner, s3Err = getReqAccessKeyV2(r)
|
cred, owner, s3Err = getReqAccessKeyV2(r)
|
||||||
case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer, authTypeStreamingUnsignedTrailer:
|
case authTypeStreamingSigned, authTypePresigned, authTypeSigned, authTypeStreamingSignedTrailer:
|
||||||
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
|
case authTypeStreamingUnsignedTrailer:
|
||||||
|
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
|
||||||
|
if s3Err == ErrMissingFields {
|
||||||
|
// Could be anonymous. cred + owner is zero value.
|
||||||
|
s3Err = ErrNone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if s3Err != ErrNone {
|
if s3Err != ErrNone {
|
||||||
return s3Err
|
return s3Err
|
||||||
|
|||||||
@@ -223,9 +223,6 @@ func (h *healingTracker) updateProgress(success, skipped bool, bytes uint64) {
|
|||||||
// update will update the tracker on the disk.
|
// update will update the tracker on the disk.
|
||||||
// If the tracker has been deleted an error is returned.
|
// If the tracker has been deleted an error is returned.
|
||||||
func (h *healingTracker) update(ctx context.Context) error {
|
func (h *healingTracker) update(ctx context.Context) error {
|
||||||
if h.disk.Healing() == nil {
|
|
||||||
return fmt.Errorf("healingTracker: drive %q is not marked as healing", h.ID)
|
|
||||||
}
|
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
if h.ID == "" || h.PoolIndex < 0 || h.SetIndex < 0 || h.DiskIndex < 0 {
|
||||||
h.ID, _ = h.disk.GetDiskID()
|
h.ID, _ = h.disk.GetDiskID()
|
||||||
|
|||||||
+14
-11
@@ -195,8 +195,8 @@ func (ef BatchJobExpireFilter) Matches(obj ObjectInfo, now time.Time) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ef.Metadata) > 0 && !obj.DeleteMarker {
|
if len(ef.Metadata) > 0 && !obj.DeleteMarker {
|
||||||
for _, kv := range ef.Metadata {
|
for _, kv := range ef.Metadata {
|
||||||
// Object (version) must match all x-amz-meta and
|
// Object (version) must match all x-amz-meta and
|
||||||
@@ -289,6 +289,16 @@ type BatchJobExpire struct {
|
|||||||
|
|
||||||
var _ yaml.Unmarshaler = &BatchJobExpire{}
|
var _ yaml.Unmarshaler = &BatchJobExpire{}
|
||||||
|
|
||||||
|
// RedactSensitive will redact any sensitive information in b.
|
||||||
|
func (r *BatchJobExpire) RedactSensitive() {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.NotificationCfg.Token != "" {
|
||||||
|
r.NotificationCfg.Token = redactedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information.
|
// UnmarshalYAML - BatchJobExpire extends default unmarshal to extract line, col information.
|
||||||
func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
|
func (r *BatchJobExpire) UnmarshalYAML(val *yaml.Node) error {
|
||||||
type expireJob BatchJobExpire
|
type expireJob BatchJobExpire
|
||||||
@@ -435,14 +445,14 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
|||||||
oiCache.Add(od, &exp.ObjectInfo)
|
oiCache.Add(od, &exp.ObjectInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
var done bool
|
|
||||||
// DeleteObject(deletePrefix: true) to expire all versions of an object
|
// DeleteObject(deletePrefix: true) to expire all versions of an object
|
||||||
for _, exp := range toExpireAll {
|
for _, exp := range toExpireAll {
|
||||||
var success bool
|
var success bool
|
||||||
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
for attempts := 1; attempts <= retryAttempts; attempts++ {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
done = true
|
ri.trackMultipleObjectVersions(exp, success)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
|
stopFn := globalBatchJobsMetrics.trace(batchJobMetricExpire, ri.JobID, attempts)
|
||||||
@@ -459,14 +469,7 @@ func batchObjsForDelete(ctx context.Context, r *BatchJobExpire, ri *batchJobInfo
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ri.trackMultipleObjectVersions(r.Bucket, exp, success)
|
ri.trackMultipleObjectVersions(exp, success)
|
||||||
if done {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if done {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMultiple objects
|
// DeleteMultiple objects
|
||||||
|
|||||||
+109
-56
@@ -39,7 +39,6 @@ import (
|
|||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/lithammer/shortuuid/v4"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
miniogo "github.com/minio/minio-go/v7"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||||
"github.com/minio/minio-go/v7/pkg/tags"
|
"github.com/minio/minio-go/v7/pkg/tags"
|
||||||
@@ -47,7 +46,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/ioutil"
|
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
"github.com/minio/pkg/v3/console"
|
"github.com/minio/pkg/v3/console"
|
||||||
"github.com/minio/pkg/v3/env"
|
"github.com/minio/pkg/v3/env"
|
||||||
@@ -61,6 +59,8 @@ var globalBatchConfig batch.Config
|
|||||||
const (
|
const (
|
||||||
// Keep the completed/failed job stats 3 days before removing it
|
// Keep the completed/failed job stats 3 days before removing it
|
||||||
oldJobsExpiration = 3 * 24 * time.Hour
|
oldJobsExpiration = 3 * 24 * time.Hour
|
||||||
|
|
||||||
|
redactedText = "**REDACTED**"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BatchJobRequest this is an internal data structure not for external consumption.
|
// BatchJobRequest this is an internal data structure not for external consumption.
|
||||||
@@ -74,6 +74,29 @@ type BatchJobRequest struct {
|
|||||||
ctx context.Context `msg:"-"`
|
ctx context.Context `msg:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RedactSensitive will redact any sensitive information in b.
|
||||||
|
func (j *BatchJobRequest) RedactSensitive() {
|
||||||
|
j.Replicate.RedactSensitive()
|
||||||
|
j.Expire.RedactSensitive()
|
||||||
|
j.KeyRotate.RedactSensitive()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedactSensitive will redact any sensitive information in b.
|
||||||
|
func (r *BatchJobReplicateV1) RedactSensitive() {
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Target.Creds.SecretKey != "" {
|
||||||
|
r.Target.Creds.SecretKey = redactedText
|
||||||
|
}
|
||||||
|
if r.Target.Creds.SessionToken != "" {
|
||||||
|
r.Target.Creds.SessionToken = redactedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedactSensitive will redact any sensitive information in b.
|
||||||
|
func (r *BatchJobKeyRotateV1) RedactSensitive() {}
|
||||||
|
|
||||||
func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
|
func notifyEndpoint(ctx context.Context, ri *batchJobInfo, endpoint, token string) error {
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -117,7 +140,7 @@ func (r BatchJobReplicateV1) Notify(ctx context.Context, ri *batchJobInfo) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
|
// ReplicateFromSource - this is not implemented yet where source is 'remote' and target is local.
|
||||||
func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
|
func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api ObjectLayer, core *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
|
||||||
srcBucket := r.Source.Bucket
|
srcBucket := r.Source.Bucket
|
||||||
tgtBucket := r.Target.Bucket
|
tgtBucket := r.Target.Bucket
|
||||||
srcObject := srcObjInfo.Name
|
srcObject := srcObjInfo.Name
|
||||||
@@ -164,7 +187,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
|
|||||||
}
|
}
|
||||||
return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
|
return r.copyWithMultipartfromSource(ctx, api, core, srcObjInfo, opts, partsCount)
|
||||||
}
|
}
|
||||||
gopts := miniogo.GetObjectOptions{
|
gopts := minio.GetObjectOptions{
|
||||||
VersionID: srcObjInfo.VersionID,
|
VersionID: srcObjInfo.VersionID,
|
||||||
}
|
}
|
||||||
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
|
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
|
||||||
@@ -185,7 +208,7 @@ func (r *BatchJobReplicateV1) ReplicateFromSource(ctx context.Context, api Objec
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
|
func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, opts ObjectOptions, partsCount int) (err error) {
|
||||||
srcBucket := r.Source.Bucket
|
srcBucket := r.Source.Bucket
|
||||||
tgtBucket := r.Target.Bucket
|
tgtBucket := r.Target.Bucket
|
||||||
srcObject := srcObjInfo.Name
|
srcObject := srcObjInfo.Name
|
||||||
@@ -226,7 +249,7 @@ func (r *BatchJobReplicateV1) copyWithMultipartfromSource(ctx context.Context, a
|
|||||||
)
|
)
|
||||||
|
|
||||||
for i := 0; i < partsCount; i++ {
|
for i := 0; i < partsCount; i++ {
|
||||||
gopts := miniogo.GetObjectOptions{
|
gopts := minio.GetObjectOptions{
|
||||||
VersionID: srcObjInfo.VersionID,
|
VersionID: srcObjInfo.VersionID,
|
||||||
PartNumber: i + 1,
|
PartNumber: i + 1,
|
||||||
}
|
}
|
||||||
@@ -301,12 +324,12 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(oi.ModTime) {
|
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(oi.ModTime) {
|
||||||
// skip all objects that are created before the specified time.
|
// skip all objects that are created before the specified time.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(oi.ModTime) {
|
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(oi.ModTime) {
|
||||||
// skip all objects that are created after the specified time.
|
// skip all objects that are created after the specified time.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -357,7 +380,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
|
|
||||||
cred := r.Source.Creds
|
cred := r.Source.Creds
|
||||||
|
|
||||||
c, err := miniogo.New(u.Host, &miniogo.Options{
|
c, err := minio.New(u.Host, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||||
Secure: u.Scheme == "https",
|
Secure: u.Scheme == "https",
|
||||||
Transport: getRemoteInstanceTransport(),
|
Transport: getRemoteInstanceTransport(),
|
||||||
@@ -368,7 +391,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
|
c.SetAppInfo("minio-"+batchJobPrefix, r.APIVersion+" "+job.ID)
|
||||||
core := &miniogo.Core{Client: c}
|
core := &minio.Core{Client: c}
|
||||||
|
|
||||||
workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
|
workerSize, err := strconv.Atoi(env.Get("_MINIO_BATCH_REPLICATION_WORKERS", strconv.Itoa(runtime.GOMAXPROCS(0)/2)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,14 +412,14 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
|
minioSrc := r.Source.Type == BatchJobReplicateResourceMinIO
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
objInfoCh := make(chan miniogo.ObjectInfo, 1)
|
objInfoCh := make(chan minio.ObjectInfo, 1)
|
||||||
go func() {
|
go func() {
|
||||||
prefixes := r.Source.Prefix.F()
|
prefixes := r.Source.Prefix.F()
|
||||||
if len(prefixes) == 0 {
|
if len(prefixes) == 0 {
|
||||||
prefixes = []string{""}
|
prefixes = []string{""}
|
||||||
}
|
}
|
||||||
for _, prefix := range prefixes {
|
for _, prefix := range prefixes {
|
||||||
prefixObjInfoCh := c.ListObjects(ctx, r.Source.Bucket, miniogo.ListObjectsOptions{
|
prefixObjInfoCh := c.ListObjects(ctx, r.Source.Bucket, minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
WithVersions: minioSrc,
|
WithVersions: minioSrc,
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
@@ -419,7 +442,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
// all user metadata or just storageClass. If its only storageClass
|
// all user metadata or just storageClass. If its only storageClass
|
||||||
// List() already returns relevant information for filter to be applied.
|
// List() already returns relevant information for filter to be applied.
|
||||||
if isMetadata && !isStorageClassOnly {
|
if isMetadata && !isStorageClassOnly {
|
||||||
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, miniogo.StatObjectOptions{})
|
oi2, err := c.StatObject(ctx, r.Source.Bucket, obj.Key, minio.StatObjectOptions{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
|
oi = toObjectInfo(r.Source.Bucket, obj.Key, oi2)
|
||||||
} else {
|
} else {
|
||||||
@@ -515,7 +538,7 @@ func (r *BatchJobReplicateV1) StartFromSource(ctx context.Context, api ObjectLay
|
|||||||
}
|
}
|
||||||
|
|
||||||
// toObjectInfo converts minio.ObjectInfo to ObjectInfo
|
// toObjectInfo converts minio.ObjectInfo to ObjectInfo
|
||||||
func toObjectInfo(bucket, object string, objInfo miniogo.ObjectInfo) ObjectInfo {
|
func toObjectInfo(bucket, object string, objInfo minio.ObjectInfo) ObjectInfo {
|
||||||
tags, _ := tags.MapToObjectTags(objInfo.UserTags)
|
tags, _ := tags.MapToObjectTags(objInfo.UserTags)
|
||||||
oi := ObjectInfo{
|
oi := ObjectInfo{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
@@ -597,12 +620,12 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
opts, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
opts, _, err := batchReplicationOpts(ctx, "", gr.ObjInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
batchLogIf(ctx, err)
|
batchLogIf(ctx, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// TODO: I am not sure we read it back, but we aren't sending whether checksums are single/multipart.
|
||||||
for k, vals := range opts.Header() {
|
for k, vals := range opts.Header() {
|
||||||
for _, v := range vals {
|
for _, v := range vals {
|
||||||
snowballObj.Headers.Add(k, v)
|
snowballObj.Headers.Add(k, v)
|
||||||
@@ -618,7 +641,7 @@ func (r BatchJobReplicateV1) writeAsArchive(ctx context.Context, objAPI ObjectLa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReplicateToTarget read from source and replicate to configured target
|
// ReplicateToTarget read from source and replicate to configured target
|
||||||
func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *miniogo.Core, srcObjInfo ObjectInfo, retry bool) error {
|
func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectLayer, c *minio.Core, srcObjInfo ObjectInfo, retry bool) error {
|
||||||
srcBucket := r.Source.Bucket
|
srcBucket := r.Source.Bucket
|
||||||
tgtBucket := r.Target.Bucket
|
tgtBucket := r.Target.Bucket
|
||||||
tgtPrefix := r.Target.Prefix
|
tgtPrefix := r.Target.Prefix
|
||||||
@@ -627,9 +650,9 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
|
|||||||
|
|
||||||
if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
|
if srcObjInfo.DeleteMarker || !srcObjInfo.VersionPurgeStatus.Empty() {
|
||||||
if retry && !s3Type {
|
if retry && !s3Type {
|
||||||
if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.StatObjectOptions{
|
if _, err := c.StatObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.StatObjectOptions{
|
||||||
VersionID: srcObjInfo.VersionID,
|
VersionID: srcObjInfo.VersionID,
|
||||||
Internal: miniogo.AdvancedGetOptions{
|
Internal: minio.AdvancedGetOptions{
|
||||||
ReplicationProxyRequest: "false",
|
ReplicationProxyRequest: "false",
|
||||||
},
|
},
|
||||||
}); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) {
|
}); isErrMethodNotAllowed(ErrorRespToObjectError(err, tgtBucket, pathJoin(tgtPrefix, srcObject))) {
|
||||||
@@ -646,19 +669,19 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
|
|||||||
dmVersionID = ""
|
dmVersionID = ""
|
||||||
versionID = ""
|
versionID = ""
|
||||||
}
|
}
|
||||||
return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), miniogo.RemoveObjectOptions{
|
return c.RemoveObject(ctx, tgtBucket, pathJoin(tgtPrefix, srcObject), minio.RemoveObjectOptions{
|
||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
Internal: miniogo.AdvancedRemoveOptions{
|
Internal: minio.AdvancedRemoveOptions{
|
||||||
ReplicationDeleteMarker: dmVersionID != "",
|
ReplicationDeleteMarker: dmVersionID != "",
|
||||||
ReplicationMTime: srcObjInfo.ModTime,
|
ReplicationMTime: srcObjInfo.ModTime,
|
||||||
ReplicationStatus: miniogo.ReplicationStatusReplica,
|
ReplicationStatus: minio.ReplicationStatusReplica,
|
||||||
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if retry && !s3Type { // when we are retrying avoid copying if necessary.
|
if retry && !s3Type { // when we are retrying avoid copying if necessary.
|
||||||
gopts := miniogo.GetObjectOptions{}
|
gopts := minio.GetObjectOptions{}
|
||||||
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
|
if err := gopts.SetMatchETag(srcObjInfo.ETag); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -687,14 +710,14 @@ func (r *BatchJobReplicateV1) ReplicateToTarget(ctx context.Context, api ObjectL
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
putOpts, err := batchReplicationOpts(ctx, "", objInfo)
|
putOpts, isMP, err := batchReplicationOpts(ctx, "", objInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
|
if r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3 {
|
||||||
putOpts.Internal = miniogo.AdvancedPutOptions{}
|
putOpts.Internal = minio.AdvancedPutOptions{}
|
||||||
}
|
}
|
||||||
if objInfo.isMultipart() {
|
if isMP {
|
||||||
if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
|
if err := replicateObjectWithMultipart(ctx, c, tgtBucket, pathJoin(tgtPrefix, objInfo.Name), rd, objInfo, putOpts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -971,7 +994,7 @@ func (ri *batchJobInfo) updateAfter(ctx context.Context, api ObjectLayer, durati
|
|||||||
// Note: to be used only with batch jobs that affect multiple versions through
|
// Note: to be used only with batch jobs that affect multiple versions through
|
||||||
// a single action. e.g batch-expire has an option to expire all versions of an
|
// a single action. e.g batch-expire has an option to expire all versions of an
|
||||||
// object which matches the given filters.
|
// object which matches the given filters.
|
||||||
func (ri *batchJobInfo) trackMultipleObjectVersions(bucket string, info ObjectInfo, success bool) {
|
func (ri *batchJobInfo) trackMultipleObjectVersions(info ObjectInfo, success bool) {
|
||||||
if success {
|
if success {
|
||||||
ri.Objects += int64(info.NumVersions)
|
ri.Objects += int64(info.NumVersions)
|
||||||
} else {
|
} else {
|
||||||
@@ -1047,12 +1070,12 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.Before(info.ModTime) {
|
if !r.Flags.Filter.CreatedAfter.IsZero() && r.Flags.Filter.CreatedAfter.After(info.ModTime) {
|
||||||
// skip all objects that are created before the specified time.
|
// skip all objects that are created before the specified time.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.After(info.ModTime) {
|
if !r.Flags.Filter.CreatedBefore.IsZero() && r.Flags.Filter.CreatedBefore.Before(info.ModTime) {
|
||||||
// skip all objects that are created after the specified time.
|
// skip all objects that are created after the specified time.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1122,8 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
|
// if one of source or target is non MinIO, just replicate the top most version like `mc mirror`
|
||||||
return !((r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3) && !info.IsLatest)
|
isSourceOrTargetS3 := r.Target.Type == BatchJobReplicateResourceS3 || r.Source.Type == BatchJobReplicateResourceS3
|
||||||
|
return !isSourceOrTargetS3 || info.IsLatest
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(r.Target.Endpoint)
|
u, err := url.Parse(r.Target.Endpoint)
|
||||||
@@ -1109,7 +1133,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
|
|
||||||
cred := r.Target.Creds
|
cred := r.Target.Creds
|
||||||
|
|
||||||
c, err := miniogo.NewCore(u.Host, &miniogo.Options{
|
c, err := minio.NewCore(u.Host, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||||
Secure: u.Scheme == "https",
|
Secure: u.Scheme == "https",
|
||||||
Transport: getRemoteInstanceTransport(),
|
Transport: getRemoteInstanceTransport(),
|
||||||
@@ -1132,14 +1156,14 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
|
if r.Source.Snowball.Disable != nil && !*r.Source.Snowball.Disable && r.Source.Type.isMinio() && r.Target.Type.isMinio() {
|
||||||
go func() {
|
go func() {
|
||||||
// Snowball currently needs the high level minio-go Client, not the Core one
|
// Snowball currently needs the high level minio-go Client, not the Core one
|
||||||
cl, err := miniogo.New(u.Host, &miniogo.Options{
|
cl, err := minio.New(u.Host, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||||
Secure: u.Scheme == "https",
|
Secure: u.Scheme == "https",
|
||||||
Transport: getRemoteInstanceTransport(),
|
Transport: getRemoteInstanceTransport(),
|
||||||
BucketLookup: lookupStyle(r.Target.Path),
|
BucketLookup: lookupStyle(r.Target.Path),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
batchLogOnceIf(ctx, err, job.ID+"miniogo.New")
|
batchLogOnceIf(ctx, err, job.ID+"minio.New")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1249,7 +1273,7 @@ func (r *BatchJobReplicateV1) Start(ctx context.Context, api ObjectLayer, job Ba
|
|||||||
stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
|
stopFn := globalBatchJobsMetrics.trace(batchJobMetricReplication, job.ID, attempts)
|
||||||
success := true
|
success := true
|
||||||
if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil {
|
if err := r.ReplicateToTarget(ctx, api, c, result, retry); err != nil {
|
||||||
if miniogo.ToErrorResponse(err).Code == "PreconditionFailed" {
|
if minio.ToErrorResponse(err).Code == "PreconditionFailed" {
|
||||||
// pre-condition failed means we already have the object copied over.
|
// pre-condition failed means we already have the object copied over.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1425,7 +1449,6 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
|
|||||||
cred = r.Source.Creds
|
cred = r.Source.Creds
|
||||||
remoteBkt = r.Source.Bucket
|
remoteBkt = r.Source.Bucket
|
||||||
pathStyle = r.Source.Path
|
pathStyle = r.Source.Path
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(remoteEp)
|
u, err := url.Parse(remoteEp)
|
||||||
@@ -1433,7 +1456,7 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := miniogo.NewCore(u.Host, &miniogo.Options{
|
c, err := minio.NewCore(u.Host, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
Creds: credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken),
|
||||||
Secure: u.Scheme == "https",
|
Secure: u.Scheme == "https",
|
||||||
Transport: getRemoteInstanceTransport(),
|
Transport: getRemoteInstanceTransport(),
|
||||||
@@ -1446,7 +1469,7 @@ func (r *BatchJobReplicateV1) Validate(ctx context.Context, job BatchJobRequest,
|
|||||||
|
|
||||||
vcfg, err := c.GetBucketVersioning(ctx, remoteBkt)
|
vcfg, err := c.GetBucketVersioning(ctx, remoteBkt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if miniogo.ToErrorResponse(err).Code == "NoSuchBucket" {
|
if minio.ToErrorResponse(err).Code == "NoSuchBucket" {
|
||||||
return batchReplicationJobError{
|
return batchReplicationJobError{
|
||||||
Code: "NoSuchTargetBucket",
|
Code: "NoSuchTargetBucket",
|
||||||
Description: "The specified target bucket does not exist",
|
Description: "The specified target bucket does not exist",
|
||||||
@@ -1551,19 +1574,19 @@ func (j *BatchJobRequest) load(ctx context.Context, api ObjectLayer, name string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts miniogo.PutObjectOptions, err error) {
|
func batchReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
|
||||||
// TODO: support custom storage class for remote replication
|
// TODO: support custom storage class for remote replication
|
||||||
putOpts, err = putReplicationOpts(ctx, "", objInfo, 0)
|
putOpts, isMP, err = putReplicationOpts(ctx, "", objInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, isMP, err
|
||||||
}
|
}
|
||||||
putOpts.Internal = miniogo.AdvancedPutOptions{
|
putOpts.Internal = minio.AdvancedPutOptions{
|
||||||
SourceVersionID: objInfo.VersionID,
|
SourceVersionID: objInfo.VersionID,
|
||||||
SourceMTime: objInfo.ModTime,
|
SourceMTime: objInfo.ModTime,
|
||||||
SourceETag: objInfo.ETag,
|
SourceETag: objInfo.ETag,
|
||||||
ReplicationRequest: true,
|
ReplicationRequest: true,
|
||||||
}
|
}
|
||||||
return putOpts, nil
|
return putOpts, isMP, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
|
// ListBatchJobs - lists all currently active batch jobs, optionally takes {jobType}
|
||||||
@@ -1577,9 +1600,6 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
jobType := r.Form.Get("jobType")
|
jobType := r.Form.Get("jobType")
|
||||||
if jobType == "" {
|
|
||||||
jobType = string(madmin.BatchJobReplicate)
|
|
||||||
}
|
|
||||||
|
|
||||||
resultCh := make(chan itemOrErr[ObjectInfo])
|
resultCh := make(chan itemOrErr[ObjectInfo])
|
||||||
|
|
||||||
@@ -1608,7 +1628,7 @@ func (a adminAPIHandlers) ListBatchJobs(w http.ResponseWriter, r *http.Request)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if jobType == string(req.Type()) {
|
if jobType == string(req.Type()) || jobType == "" {
|
||||||
listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
|
listResult.Jobs = append(listResult.Jobs, madmin.BatchJobResult{
|
||||||
ID: req.ID,
|
ID: req.ID,
|
||||||
Type: req.Type(),
|
Type: req.Type(),
|
||||||
@@ -1698,6 +1718,8 @@ func (a adminAPIHandlers) DescribeBatchJob(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove sensitive fields.
|
||||||
|
req.RedactSensitive()
|
||||||
buf, err := yaml.Marshal(req)
|
buf, err := yaml.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
batchLogIf(ctx, err)
|
batchLogIf(ctx, err)
|
||||||
@@ -1717,7 +1739,7 @@ func (a adminAPIHandlers) StartBatchJob(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf, err := io.ReadAll(ioutil.HardLimitReader(r.Body, humanize.MiByte*4))
|
buf, err := io.ReadAll(xioutil.HardLimitReader(r.Body, humanize.MiByte*4))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@@ -1805,7 +1827,7 @@ func (a adminAPIHandlers) CancelBatchJob(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, success := proxyRequestByToken(ctx, w, r, jobID); success {
|
if _, proxied, _ := proxyRequestByToken(ctx, w, r, jobID, true); proxied {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1989,7 +2011,6 @@ func (j *BatchJobPool) AddWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
job.delete(j.ctx, j.objLayer)
|
|
||||||
j.canceler(job.ID, false)
|
j.canceler(job.ID, false)
|
||||||
case <-j.workerKillCh:
|
case <-j.workerKillCh:
|
||||||
return
|
return
|
||||||
@@ -2050,7 +2071,9 @@ func (j *BatchJobPool) canceler(jobID string, cancel bool) error {
|
|||||||
canceler()
|
canceler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(j.jobCancelers, jobID)
|
if cancel {
|
||||||
|
delete(j.jobCancelers, jobID)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2176,11 +2199,42 @@ func (m *batchJobMetrics) purgeJobMetrics() {
|
|||||||
m.RUnlock()
|
m.RUnlock()
|
||||||
for _, jobID := range toDeleteJobMetrics {
|
for _, jobID := range toDeleteJobMetrics {
|
||||||
m.delete(jobID)
|
m.delete(jobID)
|
||||||
|
j := BatchJobRequest{
|
||||||
|
ID: jobID,
|
||||||
|
}
|
||||||
|
j.delete(GlobalContext, newObjectLayerFn())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// load metrics from disk on startup
|
||||||
|
func (m *batchJobMetrics) init(ctx context.Context, objectAPI ObjectLayer) error {
|
||||||
|
resultCh := make(chan itemOrErr[ObjectInfo])
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := objectAPI.Walk(ctx, minioMetaBucket, batchJobReportsPrefix, resultCh, WalkOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for result := range resultCh {
|
||||||
|
if result.Err != nil {
|
||||||
|
return result.Err
|
||||||
|
}
|
||||||
|
ri := &batchJobInfo{}
|
||||||
|
if err := ri.loadByPath(ctx, objectAPI, result.Item.Name); err != nil {
|
||||||
|
if !errors.Is(err, errNoSuchJob) {
|
||||||
|
batchLogIf(ctx, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.metrics[ri.JobID] = ri
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *batchJobMetrics) delete(jobID string) {
|
func (m *batchJobMetrics) delete(jobID string) {
|
||||||
m.Lock()
|
m.Lock()
|
||||||
defer m.Unlock()
|
defer m.Unlock()
|
||||||
@@ -2245,16 +2299,15 @@ func (m *batchJobMetrics) trace(d batchJobMetric, job string, attempts int) func
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupStyle(s string) miniogo.BucketLookupType {
|
func lookupStyle(s string) minio.BucketLookupType {
|
||||||
var lookup miniogo.BucketLookupType
|
var lookup minio.BucketLookupType
|
||||||
switch s {
|
switch s {
|
||||||
case "on":
|
case "on":
|
||||||
lookup = miniogo.BucketLookupPath
|
lookup = minio.BucketLookupPath
|
||||||
case "off":
|
case "off":
|
||||||
lookup = miniogo.BucketLookupDNS
|
lookup = minio.BucketLookupDNS
|
||||||
default:
|
default:
|
||||||
lookup = miniogo.BucketLookupAuto
|
lookup = minio.BucketLookupAuto
|
||||||
|
|
||||||
}
|
}
|
||||||
return lookup
|
return lookup
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -37,12 +38,22 @@ type streamingBitrotWriter struct {
|
|||||||
shardSize int64
|
shardSize int64
|
||||||
canClose *sync.WaitGroup
|
canClose *sync.WaitGroup
|
||||||
byteBuf []byte
|
byteBuf []byte
|
||||||
|
finished bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
|
func (b *streamingBitrotWriter) Write(p []byte) (int, error) {
|
||||||
if len(p) == 0 {
|
if len(p) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
if b.finished {
|
||||||
|
return 0, errors.New("bitrot write not allowed")
|
||||||
|
}
|
||||||
|
if int64(len(p)) > b.shardSize {
|
||||||
|
return 0, errors.New("unexpected bitrot buffer size")
|
||||||
|
}
|
||||||
|
if int64(len(p)) < b.shardSize {
|
||||||
|
b.finished = true
|
||||||
|
}
|
||||||
b.h.Reset()
|
b.h.Reset()
|
||||||
b.h.Write(p)
|
b.h.Write(p)
|
||||||
hashBytes := b.h.Sum(nil)
|
hashBytes := b.h.Sum(nil)
|
||||||
|
|||||||
+13
-7
@@ -128,14 +128,20 @@ func closeBitrotReaders(rs []io.ReaderAt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close all the writers.
|
// Close all the writers.
|
||||||
func closeBitrotWriters(ws []io.Writer) {
|
func closeBitrotWriters(ws []io.Writer) []error {
|
||||||
for _, w := range ws {
|
errs := make([]error, len(ws))
|
||||||
if w != nil {
|
for i, w := range ws {
|
||||||
if bw, ok := w.(io.Closer); ok {
|
if w == nil {
|
||||||
bw.Close()
|
errs[i] = errDiskNotFound
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
if bw, ok := w.(io.Closer); ok {
|
||||||
|
errs[i] = bw.Close()
|
||||||
|
} else {
|
||||||
|
errs[i] = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns hash sum for whole-bitrot, nil for streaming-bitrot.
|
// Returns hash sum for whole-bitrot, nil for streaming-bitrot.
|
||||||
@@ -178,7 +184,7 @@ func bitrotVerify(r io.Reader, wantSize, partSize int64, algo BitrotAlgorithm, w
|
|||||||
return errFileCorrupt
|
return errFileCorrupt
|
||||||
}
|
}
|
||||||
|
|
||||||
bufp := xioutil.ODirectPoolSmall.Get().(*[]byte)
|
bufp := xioutil.ODirectPoolSmall.Get()
|
||||||
defer xioutil.ODirectPoolSmall.Put(bufp)
|
defer xioutil.ODirectPoolSmall.Put(bufp)
|
||||||
|
|
||||||
for left > 0 {
|
for left > 0 {
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ func (bs *bootstrapTracer) Events() []madmin.TraceInfo {
|
|||||||
traceInfo := make([]madmin.TraceInfo, 0, bootstrapTraceLimit)
|
traceInfo := make([]madmin.TraceInfo, 0, bootstrapTraceLimit)
|
||||||
|
|
||||||
bs.mu.RLock()
|
bs.mu.RLock()
|
||||||
for _, i := range bs.info {
|
traceInfo = append(traceInfo, bs.info...)
|
||||||
traceInfo = append(traceInfo, i)
|
|
||||||
}
|
|
||||||
bs.mu.RUnlock()
|
bs.mu.RUnlock()
|
||||||
|
|
||||||
return traceInfo
|
return traceInfo
|
||||||
|
|||||||
@@ -183,7 +183,12 @@ var binaryChecksum = getBinaryChecksum()
|
|||||||
|
|
||||||
func getBinaryChecksum() string {
|
func getBinaryChecksum() string {
|
||||||
mw := md5.New()
|
mw := md5.New()
|
||||||
b, err := os.Open(os.Args[0])
|
binPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Calculating checksum failed: %s", err)
|
||||||
|
return "00000000000000000000000000000000"
|
||||||
|
}
|
||||||
|
b, err := os.Open(binPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Calculating checksum failed: %s", err)
|
logger.Error("Calculating checksum failed: %s", err)
|
||||||
return "00000000000000000000000000000000"
|
return "00000000000000000000000000000000"
|
||||||
|
|||||||
@@ -344,11 +344,9 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
|
|||||||
Created: dnsRecords[0].CreationDate,
|
Created: dnsRecords[0].CreationDate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(bucketsInfo, func(i, j int) bool {
|
sort.Slice(bucketsInfo, func(i, j int) bool {
|
||||||
return bucketsInfo[i].Name < bucketsInfo[j].Name
|
return bucketsInfo[i].Name < bucketsInfo[j].Name
|
||||||
})
|
})
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Invoke the list buckets.
|
// Invoke the list buckets.
|
||||||
var err error
|
var err error
|
||||||
@@ -429,7 +427,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
|
|
||||||
// Content-Md5 is required should be set
|
// Content-Md5 is required should be set
|
||||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
if !validateLengthAndChecksum(r) {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -561,7 +559,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
}, goi, opts, gerr)
|
}, goi, opts, gerr)
|
||||||
if dsc.ReplicateAny() {
|
if dsc.ReplicateAny() {
|
||||||
if object.VersionID != "" {
|
if object.VersionID != "" {
|
||||||
object.VersionPurgeStatus = Pending
|
object.VersionPurgeStatus = replication.VersionPurgePending
|
||||||
object.VersionPurgeStatuses = dsc.PendingStatus()
|
object.VersionPurgeStatuses = dsc.PendingStatus()
|
||||||
} else {
|
} else {
|
||||||
object.DeleteMarkerReplicationStatus = dsc.PendingStatus()
|
object.DeleteMarkerReplicationStatus = dsc.PendingStatus()
|
||||||
@@ -671,7 +669,7 @@ func (api objectAPIHandlers) DeleteMultipleObjectsHandler(w http.ResponseWriter,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == Pending) {
|
if replicateDeletes && (dobj.DeleteMarkerReplicationStatus() == replication.Pending || dobj.VersionPurgeStatus() == replication.VersionPurgePending) {
|
||||||
// copy so we can re-add null ID.
|
// copy so we can re-add null ID.
|
||||||
dobj := dobj
|
dobj := dobj
|
||||||
if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
|
if isDirObject(dobj.ObjectName) && dobj.VersionID == "" {
|
||||||
@@ -841,7 +839,6 @@ func (api objectAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
apiErr := ErrBucketAlreadyExists
|
apiErr := ErrBucketAlreadyExists
|
||||||
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
if !globalDomainIPs.Intersection(set.CreateStringSet(getHostsSlice(sr)...)).IsEmpty() {
|
||||||
@@ -1338,6 +1335,7 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
opts.EncryptFn = metadataEncrypter(objectEncryptionKey)
|
||||||
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
|
pReader, err = pReader.WithEncryption(hashReader, &objectEncryptionKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
@@ -1809,6 +1807,10 @@ func (api objectAPIHandlers) PutBucketObjectLockConfigHandler(w http.ResponseWri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audit log tags.
|
||||||
|
reqInfo := logger.GetReqInfo(ctx)
|
||||||
|
reqInfo.SetTags("retention", config.String())
|
||||||
|
|
||||||
configData, err := xml.Marshal(config)
|
configData, err := xml.Marshal(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
|
|||||||
@@ -188,7 +188,6 @@ func testGetBucketLocationHandler(obj ObjectLayer, instanceType, bucketName stri
|
|||||||
if errorResponse.Code != testCase.errorResponse.Code {
|
if errorResponse.Code != testCase.errorResponse.Code {
|
||||||
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
|
t.Errorf("Test %d: %s: Expected the error code to be `%s`, but instead found `%s`", i+1, instanceType, testCase.errorResponse.Code, errorResponse.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test for Anonymous/unsigned http request.
|
// Test for Anonymous/unsigned http request.
|
||||||
@@ -290,7 +289,6 @@ func testHeadBucketHandler(obj ObjectLayer, instanceType, bucketName string, api
|
|||||||
if recV2.Code != testCase.expectedRespStatus {
|
if recV2.Code != testCase.expectedRespStatus {
|
||||||
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code)
|
t.Errorf("Test %d: %s: Expected the response status to be `%d`, but instead found `%d`", i+1, instanceType, testCase.expectedRespStatus, recV2.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test for Anonymous/unsigned http request.
|
// Test for Anonymous/unsigned http request.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
//go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE
|
//go:generate stringer -type lcEventSrc -trimprefix lcEventSrc_ $GOFILE
|
||||||
type lcEventSrc uint8
|
type lcEventSrc uint8
|
||||||
|
|
||||||
//revive:disable:var-naming Underscores is used here to indicate where common prefix ends and the enumeration name begins
|
//nolint:staticcheck,revive // Underscores are used here to indicate where common prefix ends and the enumeration name begins
|
||||||
const (
|
const (
|
||||||
lcEventSrc_None lcEventSrc = iota
|
lcEventSrc_None lcEventSrc = iota
|
||||||
lcEventSrc_Heal
|
lcEventSrc_Heal
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@@ -53,7 +52,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
// PutBucketLifecycle always needs a Content-Md5
|
// PutBucketLifecycle always needs a Content-Md5
|
||||||
if _, ok := r.Header[xhttp.ContentMD5]; !ok {
|
if !validateLengthAndChecksum(r) {
|
||||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMissingContentMD5), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,7 +69,7 @@ func (api objectAPIHandlers) PutBucketLifecycleHandler(w http.ResponseWriter, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(io.LimitReader(r.Body, r.ContentLength))
|
bucketLifecycle, err := lifecycle.ParseLifecycleConfigWithID(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
|
|||||||
+43
-31
@@ -71,8 +71,12 @@ func NewLifecycleSys() *LifecycleSys {
|
|||||||
return &LifecycleSys{}
|
return &LifecycleSys{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string) madmin.TraceInfo {
|
func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event string, metadata map[string]string, err string) madmin.TraceInfo {
|
||||||
sz, _ := oi.GetActualSize()
|
sz, _ := oi.GetActualSize()
|
||||||
|
if metadata == nil {
|
||||||
|
metadata = make(map[string]string)
|
||||||
|
}
|
||||||
|
metadata["version-id"] = oi.VersionID
|
||||||
return madmin.TraceInfo{
|
return madmin.TraceInfo{
|
||||||
TraceType: madmin.TraceILM,
|
TraceType: madmin.TraceILM,
|
||||||
Time: startTime,
|
Time: startTime,
|
||||||
@@ -81,18 +85,22 @@ func ilmTrace(startTime time.Time, duration time.Duration, oi ObjectInfo, event
|
|||||||
Duration: duration,
|
Duration: duration,
|
||||||
Path: pathJoin(oi.Bucket, oi.Name),
|
Path: pathJoin(oi.Bucket, oi.Name),
|
||||||
Bytes: sz,
|
Bytes: sz,
|
||||||
Error: "",
|
Error: err,
|
||||||
Message: getSource(4),
|
Message: getSource(4),
|
||||||
Custom: metadata,
|
Custom: metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string) {
|
func (sys *LifecycleSys) trace(oi ObjectInfo) func(event string, metadata map[string]string, err error) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
return func(event string, metadata map[string]string) {
|
return func(event string, metadata map[string]string, err error) {
|
||||||
duration := time.Since(startTime)
|
duration := time.Since(startTime)
|
||||||
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
|
if globalTrace.NumSubscribers(madmin.TraceILM) > 0 {
|
||||||
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata))
|
e := ""
|
||||||
|
if err != nil {
|
||||||
|
e = err.Error()
|
||||||
|
}
|
||||||
|
globalTrace.Publish(ilmTrace(startTime, duration, oi, event, metadata, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,8 +155,8 @@ func (f freeVersionTask) OpHash() uint64 {
|
|||||||
return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name)
|
return xxh3.HashString(f.TransitionedObject.Tier + f.TransitionedObject.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n newerNoncurrentTask) OpHash() uint64 {
|
func (n noncurrentVersionsTask) OpHash() uint64 {
|
||||||
return xxh3.HashString(n.bucket + n.versions[0].ObjectV.ObjectName)
|
return xxh3.HashString(n.bucket + n.versions[0].ObjectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j jentry) OpHash() uint64 {
|
func (j jentry) OpHash() uint64 {
|
||||||
@@ -232,14 +240,16 @@ func (es *expiryState) enqueueByDays(oi ObjectInfo, event lifecycle.Event, src l
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// enqueueByNewerNoncurrent enqueues object versions expired by
|
func (es *expiryState) enqueueNoncurrentVersions(bucket string, versions []ObjectToDelete, events []lifecycle.Event) {
|
||||||
// NewerNoncurrentVersions limit for expiry.
|
|
||||||
func (es *expiryState) enqueueByNewerNoncurrent(bucket string, versions []ObjectToDelete, lcEvent lifecycle.Event) {
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
task := newerNoncurrentTask{bucket: bucket, versions: versions, event: lcEvent}
|
task := noncurrentVersionsTask{
|
||||||
|
bucket: bucket,
|
||||||
|
versions: versions,
|
||||||
|
events: events,
|
||||||
|
}
|
||||||
wrkr := es.getWorkerCh(task.OpHash())
|
wrkr := es.getWorkerCh(task.OpHash())
|
||||||
if wrkr == nil {
|
if wrkr == nil {
|
||||||
es.stats.missedExpiryTasks.Add(1)
|
es.stats.missedExpiryTasks.Add(1)
|
||||||
@@ -339,8 +349,8 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
|
|||||||
} else {
|
} else {
|
||||||
applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src)
|
applyExpiryOnNonTransitionedObjects(es.ctx, es.objAPI, v.objInfo, v.event, v.src)
|
||||||
}
|
}
|
||||||
case newerNoncurrentTask:
|
case noncurrentVersionsTask:
|
||||||
deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.event)
|
deleteObjectVersions(es.ctx, es.objAPI, v.bucket, v.versions, v.events)
|
||||||
case jentry:
|
case jentry:
|
||||||
transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName))
|
transitionLogIf(es.ctx, deleteObjectFromRemoteTier(es.ctx, v.ObjName, v.VersionID, v.TierName))
|
||||||
case freeVersionTask:
|
case freeVersionTask:
|
||||||
@@ -348,7 +358,7 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
|
|||||||
traceFn := globalLifecycleSys.trace(oi)
|
traceFn := globalLifecycleSys.trace(oi)
|
||||||
if !oi.TransitionedObject.FreeVersion {
|
if !oi.TransitionedObject.FreeVersion {
|
||||||
// nothing to be done
|
// nothing to be done
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreNotFoundErr := func(err error) error {
|
ignoreNotFoundErr := func(err error) error {
|
||||||
@@ -362,7 +372,8 @@ func (es *expiryState) Worker(input <-chan expiryOp) {
|
|||||||
err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
|
err := deleteObjectFromRemoteTier(es.ctx, oi.TransitionedObject.Name, oi.TransitionedObject.VersionID, oi.TransitionedObject.Tier)
|
||||||
if ignoreNotFoundErr(err) != nil {
|
if ignoreNotFoundErr(err) != nil {
|
||||||
transitionLogIf(es.ctx, err)
|
transitionLogIf(es.ctx, err)
|
||||||
return
|
traceFn(ILMFreeVersionDelete, nil, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove this free version
|
// Remove this free version
|
||||||
@@ -387,12 +398,10 @@ func initBackgroundExpiry(ctx context.Context, objectAPI ObjectLayer) {
|
|||||||
globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers())
|
globalExpiryState = newExpiryState(ctx, objectAPI, globalILMConfig.getExpirationWorkers())
|
||||||
}
|
}
|
||||||
|
|
||||||
// newerNoncurrentTask encapsulates arguments required by worker to expire objects
|
type noncurrentVersionsTask struct {
|
||||||
// by NewerNoncurrentVersions
|
|
||||||
type newerNoncurrentTask struct {
|
|
||||||
bucket string
|
bucket string
|
||||||
versions []ObjectToDelete
|
versions []ObjectToDelete
|
||||||
event lifecycle.Event
|
events []lifecycle.Event
|
||||||
}
|
}
|
||||||
|
|
||||||
type transitionTask struct {
|
type transitionTask struct {
|
||||||
@@ -1099,17 +1108,20 @@ func isRestoredObjectOnDisk(meta map[string]string) (onDisk bool) {
|
|||||||
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
|
// ToLifecycleOpts returns lifecycle.ObjectOpts value for oi.
|
||||||
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
func (oi ObjectInfo) ToLifecycleOpts() lifecycle.ObjectOpts {
|
||||||
return lifecycle.ObjectOpts{
|
return lifecycle.ObjectOpts{
|
||||||
Name: oi.Name,
|
Name: oi.Name,
|
||||||
UserTags: oi.UserTags,
|
UserTags: oi.UserTags,
|
||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
ModTime: oi.ModTime,
|
ModTime: oi.ModTime,
|
||||||
Size: oi.Size,
|
Size: oi.Size,
|
||||||
IsLatest: oi.IsLatest,
|
IsLatest: oi.IsLatest,
|
||||||
NumVersions: oi.NumVersions,
|
NumVersions: oi.NumVersions,
|
||||||
DeleteMarker: oi.DeleteMarker,
|
DeleteMarker: oi.DeleteMarker,
|
||||||
SuccessorModTime: oi.SuccessorModTime,
|
SuccessorModTime: oi.SuccessorModTime,
|
||||||
RestoreOngoing: oi.RestoreOngoing,
|
RestoreOngoing: oi.RestoreOngoing,
|
||||||
RestoreExpires: oi.RestoreExpires,
|
RestoreExpires: oi.RestoreExpires,
|
||||||
TransitionStatus: oi.TransitionedObject.Status,
|
TransitionStatus: oi.TransitionedObject.Status,
|
||||||
|
UserDefined: oi.UserDefined,
|
||||||
|
VersionPurgeStatus: oi.VersionPurgeStatus,
|
||||||
|
ReplicationStatus: oi.ReplicationStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,26 +243,26 @@ func parseRequestToken(token string) (subToken string, nodeIndex int) {
|
|||||||
return subToken, nodeIndex
|
return subToken, nodeIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string) (string, bool) {
|
func proxyRequestByToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token string, returnErr bool) (subToken string, proxied bool, success bool) {
|
||||||
subToken, nodeIndex := parseRequestToken(token)
|
var nodeIndex int
|
||||||
if nodeIndex > 0 {
|
if subToken, nodeIndex = parseRequestToken(token); nodeIndex >= 0 {
|
||||||
return subToken, proxyRequestByNodeIndex(ctx, w, r, nodeIndex)
|
proxied, success = proxyRequestByNodeIndex(ctx, w, r, nodeIndex, returnErr)
|
||||||
}
|
}
|
||||||
return subToken, false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int) (success bool) {
|
func proxyRequestByNodeIndex(ctx context.Context, w http.ResponseWriter, r *http.Request, index int, returnErr bool) (proxied, success bool) {
|
||||||
if len(globalProxyEndpoints) == 0 {
|
if len(globalProxyEndpoints) == 0 {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
if index < 0 || index >= len(globalProxyEndpoints) {
|
if index < 0 || index >= len(globalProxyEndpoints) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
ep := globalProxyEndpoints[index]
|
ep := globalProxyEndpoints[index]
|
||||||
if ep.IsLocal {
|
if ep.IsLocal {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
return proxyRequest(ctx, w, r, ep)
|
return true, proxyRequest(ctx, w, r, ep, returnErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
||||||
|
|||||||
@@ -647,9 +647,7 @@ func (sys *BucketMetadataSys) init(ctx context.Context, buckets []string) {
|
|||||||
// Reset the state of the BucketMetadataSys.
|
// Reset the state of the BucketMetadataSys.
|
||||||
func (sys *BucketMetadataSys) Reset() {
|
func (sys *BucketMetadataSys) Reset() {
|
||||||
sys.Lock()
|
sys.Lock()
|
||||||
for k := range sys.metadataMap {
|
clear(sys.metadataMap)
|
||||||
delete(sys.metadataMap, k)
|
|
||||||
}
|
|
||||||
sys.Unlock()
|
sys.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob
|
|||||||
if legalHoldRequested {
|
if legalHoldRequested {
|
||||||
var lerr error
|
var lerr error
|
||||||
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(rq.Header); lerr != nil {
|
if legalHold, lerr = objectlock.ParseObjectLockLegalHoldHeaders(rq.Header); lerr != nil {
|
||||||
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, lerr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +305,7 @@ func checkPutObjectLockAllowed(ctx context.Context, rq *http.Request, bucket, ob
|
|||||||
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header)
|
rMode, rDate, err := objectlock.ParseObjectLockRetentionHeaders(rq.Header)
|
||||||
if err != nil && !(replica && rMode == "" && rDate.IsZero()) {
|
if err != nil && (!replica || rMode != "" || !rDate.IsZero()) {
|
||||||
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
return mode, retainDate, legalHold, toAPIErrorCode(ctx, err)
|
||||||
}
|
}
|
||||||
if retentionPermErr != ErrNone {
|
if retentionPermErr != ErrNone {
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ func (r *ReplicationStats) collectWorkerMetrics(ctx context.Context) {
|
|||||||
r.wlock.Lock()
|
r.wlock.Lock()
|
||||||
r.workers.update()
|
r.workers.update()
|
||||||
r.wlock.Unlock()
|
r.wlock.Unlock()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,16 +125,16 @@ func (ri replicatedInfos) VersionPurgeStatus() VersionPurgeStatusType {
|
|||||||
completed := 0
|
completed := 0
|
||||||
for _, v := range ri.Targets {
|
for _, v := range ri.Targets {
|
||||||
switch v.VersionPurgeStatus {
|
switch v.VersionPurgeStatus {
|
||||||
case Failed:
|
case replication.VersionPurgeFailed:
|
||||||
return Failed
|
return replication.VersionPurgeFailed
|
||||||
case Complete:
|
case replication.VersionPurgeComplete:
|
||||||
completed++
|
completed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if completed == len(ri.Targets) {
|
if completed == len(ri.Targets) {
|
||||||
return Complete
|
return replication.VersionPurgeComplete
|
||||||
}
|
}
|
||||||
return Pending
|
return replication.VersionPurgePending
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ri replicatedInfos) VersionPurgeStatusInternal() string {
|
func (ri replicatedInfos) VersionPurgeStatusInternal() string {
|
||||||
@@ -380,7 +380,7 @@ func (rs *ReplicationState) CompositeReplicationStatus() (st replication.StatusT
|
|||||||
// CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated.
|
// CompositeVersionPurgeStatus returns overall replication purge status for the permanent delete being replicated.
|
||||||
func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType {
|
func (rs *ReplicationState) CompositeVersionPurgeStatus() VersionPurgeStatusType {
|
||||||
switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) {
|
switch VersionPurgeStatusType(rs.VersionPurgeStatusInternal) {
|
||||||
case Pending, Complete, Failed: // for backward compatibility
|
case replication.VersionPurgePending, replication.VersionPurgeComplete, replication.VersionPurgeFailed: // for backward compatibility
|
||||||
return VersionPurgeStatusType(rs.VersionPurgeStatusInternal)
|
return VersionPurgeStatusType(rs.VersionPurgeStatusInternal)
|
||||||
default:
|
default:
|
||||||
return getCompositeVersionPurgeStatus(rs.PurgeTargets)
|
return getCompositeVersionPurgeStatus(rs.PurgeTargets)
|
||||||
@@ -478,16 +478,16 @@ func getCompositeVersionPurgeStatus(m map[string]VersionPurgeStatusType) Version
|
|||||||
completed := 0
|
completed := 0
|
||||||
for _, v := range m {
|
for _, v := range m {
|
||||||
switch v {
|
switch v {
|
||||||
case Failed:
|
case replication.VersionPurgeFailed:
|
||||||
return Failed
|
return replication.VersionPurgeFailed
|
||||||
case Complete:
|
case replication.VersionPurgeComplete:
|
||||||
completed++
|
completed++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if completed == len(m) {
|
if completed == len(m) {
|
||||||
return Complete
|
return replication.VersionPurgeComplete
|
||||||
}
|
}
|
||||||
return Pending
|
return replication.VersionPurgePending
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
|
// getHealReplicateObjectInfo returns info needed by heal replication in ReplicateObjectInfo
|
||||||
@@ -635,28 +635,7 @@ type ResyncTarget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication
|
// VersionPurgeStatusType represents status of a versioned delete or permanent delete w.r.t bucket replication
|
||||||
type VersionPurgeStatusType string
|
type VersionPurgeStatusType = replication.VersionPurgeStatusType
|
||||||
|
|
||||||
const (
|
|
||||||
// Pending - versioned delete replication is pending.
|
|
||||||
Pending VersionPurgeStatusType = "PENDING"
|
|
||||||
|
|
||||||
// Complete - versioned delete replication is now complete, erase version on disk.
|
|
||||||
Complete VersionPurgeStatusType = "COMPLETE"
|
|
||||||
|
|
||||||
// Failed - versioned delete replication failed.
|
|
||||||
Failed VersionPurgeStatusType = "FAILED"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Empty returns true if purge status was not set.
|
|
||||||
func (v VersionPurgeStatusType) Empty() bool {
|
|
||||||
return string(v) == ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pending returns true if the version is pending purge.
|
|
||||||
func (v VersionPurgeStatusType) Pending() bool {
|
|
||||||
return v == Pending || v == Failed
|
|
||||||
}
|
|
||||||
|
|
||||||
type replicationResyncer struct {
|
type replicationResyncer struct {
|
||||||
// map of bucket to their resync status
|
// map of bucket to their resync status
|
||||||
|
|||||||
@@ -915,33 +915,29 @@ func (z *ReplicationState) DecodeMsg(dc *msgp.Reader) (err error) {
|
|||||||
err = msgp.WrapError(err, "PurgeTargets")
|
err = msgp.WrapError(err, "PurgeTargets")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
{
|
err = za0004.DecodeMsg(dc)
|
||||||
var zb0004 string
|
if err != nil {
|
||||||
zb0004, err = dc.ReadString()
|
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
||||||
if err != nil {
|
return
|
||||||
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
za0004 = VersionPurgeStatusType(zb0004)
|
|
||||||
}
|
}
|
||||||
z.PurgeTargets[za0003] = za0004
|
z.PurgeTargets[za0003] = za0004
|
||||||
}
|
}
|
||||||
case "ResetStatusesMap":
|
case "ResetStatusesMap":
|
||||||
var zb0005 uint32
|
var zb0004 uint32
|
||||||
zb0005, err = dc.ReadMapHeader()
|
zb0004, err = dc.ReadMapHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "ResetStatusesMap")
|
err = msgp.WrapError(err, "ResetStatusesMap")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if z.ResetStatusesMap == nil {
|
if z.ResetStatusesMap == nil {
|
||||||
z.ResetStatusesMap = make(map[string]string, zb0005)
|
z.ResetStatusesMap = make(map[string]string, zb0004)
|
||||||
} else if len(z.ResetStatusesMap) > 0 {
|
} else if len(z.ResetStatusesMap) > 0 {
|
||||||
for key := range z.ResetStatusesMap {
|
for key := range z.ResetStatusesMap {
|
||||||
delete(z.ResetStatusesMap, key)
|
delete(z.ResetStatusesMap, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for zb0005 > 0 {
|
for zb0004 > 0 {
|
||||||
zb0005--
|
zb0004--
|
||||||
var za0005 string
|
var za0005 string
|
||||||
var za0006 string
|
var za0006 string
|
||||||
za0005, err = dc.ReadString()
|
za0005, err = dc.ReadString()
|
||||||
@@ -1078,7 +1074,7 @@ func (z *ReplicationState) EncodeMsg(en *msgp.Writer) (err error) {
|
|||||||
err = msgp.WrapError(err, "PurgeTargets")
|
err = msgp.WrapError(err, "PurgeTargets")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = en.WriteString(string(za0004))
|
err = za0004.EncodeMsg(en)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
||||||
return
|
return
|
||||||
@@ -1154,7 +1150,11 @@ func (z *ReplicationState) MarshalMsg(b []byte) (o []byte, err error) {
|
|||||||
o = msgp.AppendMapHeader(o, uint32(len(z.PurgeTargets)))
|
o = msgp.AppendMapHeader(o, uint32(len(z.PurgeTargets)))
|
||||||
for za0003, za0004 := range z.PurgeTargets {
|
for za0003, za0004 := range z.PurgeTargets {
|
||||||
o = msgp.AppendString(o, za0003)
|
o = msgp.AppendString(o, za0003)
|
||||||
o = msgp.AppendString(o, string(za0004))
|
o, err = za0004.MarshalMsg(o)
|
||||||
|
if err != nil {
|
||||||
|
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// string "ResetStatusesMap"
|
// string "ResetStatusesMap"
|
||||||
o = append(o, 0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70)
|
o = append(o, 0xb0, 0x52, 0x65, 0x73, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x4d, 0x61, 0x70)
|
||||||
@@ -1279,35 +1279,31 @@ func (z *ReplicationState) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|||||||
err = msgp.WrapError(err, "PurgeTargets")
|
err = msgp.WrapError(err, "PurgeTargets")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
{
|
bts, err = za0004.UnmarshalMsg(bts)
|
||||||
var zb0004 string
|
if err != nil {
|
||||||
zb0004, bts, err = msgp.ReadStringBytes(bts)
|
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
||||||
if err != nil {
|
return
|
||||||
err = msgp.WrapError(err, "PurgeTargets", za0003)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
za0004 = VersionPurgeStatusType(zb0004)
|
|
||||||
}
|
}
|
||||||
z.PurgeTargets[za0003] = za0004
|
z.PurgeTargets[za0003] = za0004
|
||||||
}
|
}
|
||||||
case "ResetStatusesMap":
|
case "ResetStatusesMap":
|
||||||
var zb0005 uint32
|
var zb0004 uint32
|
||||||
zb0005, bts, err = msgp.ReadMapHeaderBytes(bts)
|
zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "ResetStatusesMap")
|
err = msgp.WrapError(err, "ResetStatusesMap")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if z.ResetStatusesMap == nil {
|
if z.ResetStatusesMap == nil {
|
||||||
z.ResetStatusesMap = make(map[string]string, zb0005)
|
z.ResetStatusesMap = make(map[string]string, zb0004)
|
||||||
} else if len(z.ResetStatusesMap) > 0 {
|
} else if len(z.ResetStatusesMap) > 0 {
|
||||||
for key := range z.ResetStatusesMap {
|
for key := range z.ResetStatusesMap {
|
||||||
delete(z.ResetStatusesMap, key)
|
delete(z.ResetStatusesMap, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for zb0005 > 0 {
|
for zb0004 > 0 {
|
||||||
var za0005 string
|
var za0005 string
|
||||||
var za0006 string
|
var za0006 string
|
||||||
zb0005--
|
zb0004--
|
||||||
za0005, bts, err = msgp.ReadStringBytes(bts)
|
za0005, bts, err = msgp.ReadStringBytes(bts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = msgp.WrapError(err, "ResetStatusesMap")
|
err = msgp.WrapError(err, "ResetStatusesMap")
|
||||||
@@ -1345,7 +1341,7 @@ func (z *ReplicationState) Msgsize() (s int) {
|
|||||||
if z.PurgeTargets != nil {
|
if z.PurgeTargets != nil {
|
||||||
for za0003, za0004 := range z.PurgeTargets {
|
for za0003, za0004 := range z.PurgeTargets {
|
||||||
_ = za0004
|
_ = za0004
|
||||||
s += msgp.StringPrefixSize + len(za0003) + msgp.StringPrefixSize + len(string(za0004))
|
s += msgp.StringPrefixSize + len(za0003) + za0004.Msgsize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s += 17 + msgp.MapHeaderSize
|
s += 17 + msgp.MapHeaderSize
|
||||||
@@ -2507,55 +2503,3 @@ func (z *TargetReplicationResyncStatus) Msgsize() (s int) {
|
|||||||
s = 1 + 3 + msgp.TimeSize + 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object)
|
s = 1 + 3 + msgp.TimeSize + 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(z.ResyncID) + 4 + msgp.TimeSize + 4 + msgp.IntSize + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 3 + msgp.Int64Size + 4 + msgp.Int64Size + 4 + msgp.StringPrefixSize + len(z.Bucket) + 4 + msgp.StringPrefixSize + len(z.Object)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeMsg implements msgp.Decodable
|
|
||||||
func (z *VersionPurgeStatusType) DecodeMsg(dc *msgp.Reader) (err error) {
|
|
||||||
{
|
|
||||||
var zb0001 string
|
|
||||||
zb0001, err = dc.ReadString()
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(*z) = VersionPurgeStatusType(zb0001)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeMsg implements msgp.Encodable
|
|
||||||
func (z VersionPurgeStatusType) EncodeMsg(en *msgp.Writer) (err error) {
|
|
||||||
err = en.WriteString(string(z))
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalMsg implements msgp.Marshaler
|
|
||||||
func (z VersionPurgeStatusType) MarshalMsg(b []byte) (o []byte, err error) {
|
|
||||||
o = msgp.Require(b, z.Msgsize())
|
|
||||||
o = msgp.AppendString(o, string(z))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalMsg implements msgp.Unmarshaler
|
|
||||||
func (z *VersionPurgeStatusType) UnmarshalMsg(bts []byte) (o []byte, err error) {
|
|
||||||
{
|
|
||||||
var zb0001 string
|
|
||||||
zb0001, bts, err = msgp.ReadStringBytes(bts)
|
|
||||||
if err != nil {
|
|
||||||
err = msgp.WrapError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
(*z) = VersionPurgeStatusType(zb0001)
|
|
||||||
}
|
|
||||||
o = bts
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
|
|
||||||
func (z VersionPurgeStatusType) Msgsize() (s int) {
|
|
||||||
s = msgp.StringPrefixSize + len(string(z))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
+108
-45
@@ -49,12 +49,11 @@ import (
|
|||||||
"github.com/minio/minio/internal/hash"
|
"github.com/minio/minio/internal/hash"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
xioutil "github.com/minio/minio/internal/ioutil"
|
xioutil "github.com/minio/minio/internal/ioutil"
|
||||||
|
"github.com/minio/minio/internal/kms"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/once"
|
"github.com/minio/minio/internal/once"
|
||||||
"github.com/tinylib/msgp/msgp"
|
"github.com/tinylib/msgp/msgp"
|
||||||
"github.com/zeebo/xxh3"
|
"github.com/zeebo/xxh3"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -147,7 +146,7 @@ func validateReplicationDestination(ctx context.Context, bucket string, rCfg *re
|
|||||||
if errInt == nil {
|
if errInt == nil {
|
||||||
err = nil
|
err = nil
|
||||||
} else {
|
} else {
|
||||||
err = errInt.(error)
|
err, _ = errInt.(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch err.(type) {
|
switch err.(type) {
|
||||||
@@ -391,7 +390,7 @@ func checkReplicateDelete(ctx context.Context, bucket string, dobj ObjectToDelet
|
|||||||
// can be the case that other cluster is down and duplicate `mc rm --vid`
|
// can be the case that other cluster is down and duplicate `mc rm --vid`
|
||||||
// is issued - this still needs to be replicated back to the other target
|
// is issued - this still needs to be replicated back to the other target
|
||||||
if !oi.VersionPurgeStatus.Empty() {
|
if !oi.VersionPurgeStatus.Empty() {
|
||||||
replicate = oi.VersionPurgeStatus == Pending || oi.VersionPurgeStatus == Failed
|
replicate = oi.VersionPurgeStatus == replication.VersionPurgePending || oi.VersionPurgeStatus == replication.VersionPurgeFailed
|
||||||
dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
|
dsc.Set(newReplicateTargetDecision(tgtArn, replicate, sync))
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -619,7 +618,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
rinfo.ReplicationStatus = rinfo.PrevReplicationStatus
|
rinfo.ReplicationStatus = rinfo.PrevReplicationStatus
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if dobj.VersionID != "" && rinfo.VersionPurgeStatus == Complete {
|
if dobj.VersionID != "" && rinfo.VersionPurgeStatus == replication.VersionPurgeComplete {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
if globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||||
@@ -639,7 +638,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
if dobj.VersionID == "" {
|
if dobj.VersionID == "" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
} else {
|
} else {
|
||||||
rinfo.VersionPurgeStatus = Failed
|
rinfo.VersionPurgeStatus = replication.VersionPurgeFailed
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -663,7 +662,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
case isErrObjectNotFound(serr), isErrVersionNotFound(serr):
|
case isErrObjectNotFound(serr), isErrVersionNotFound(serr):
|
||||||
// version being purged is already not found on target.
|
// version being purged is already not found on target.
|
||||||
if !rinfo.VersionPurgeStatus.Empty() {
|
if !rinfo.VersionPurgeStatus.Empty() {
|
||||||
rinfo.VersionPurgeStatus = Complete
|
rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case isErrReadQuorum(serr), isErrWriteQuorum(serr):
|
case isErrReadQuorum(serr), isErrWriteQuorum(serr):
|
||||||
@@ -696,7 +695,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
if dobj.VersionID == "" {
|
if dobj.VersionID == "" {
|
||||||
rinfo.ReplicationStatus = replication.Failed
|
rinfo.ReplicationStatus = replication.Failed
|
||||||
} else {
|
} else {
|
||||||
rinfo.VersionPurgeStatus = Failed
|
rinfo.VersionPurgeStatus = replication.VersionPurgeFailed
|
||||||
}
|
}
|
||||||
replLogIf(ctx, fmt.Errorf("unable to replicate delete marker to %s: %s/%s(%s): %w", tgt.EndpointURL(), tgt.Bucket, dobj.ObjectName, versionID, rmErr))
|
replLogIf(ctx, fmt.Errorf("unable to replicate delete marker to %s: %s/%s(%s): %w", tgt.EndpointURL(), tgt.Bucket, dobj.ObjectName, versionID, rmErr))
|
||||||
if rmErr != nil && minio.IsNetworkOrHostDown(rmErr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
if rmErr != nil && minio.IsNetworkOrHostDown(rmErr, true) && !globalBucketTargetSys.isOffline(tgt.EndpointURL()) {
|
||||||
@@ -706,7 +705,7 @@ func replicateDeleteToTarget(ctx context.Context, dobj DeletedObjectReplicationI
|
|||||||
if dobj.VersionID == "" {
|
if dobj.VersionID == "" {
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
} else {
|
} else {
|
||||||
rinfo.VersionPurgeStatus = Complete
|
rinfo.VersionPurgeStatus = replication.VersionPurgeComplete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -775,13 +774,14 @@ func (m caseInsensitiveMap) Lookup(key string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, partNum int) (putOpts minio.PutObjectOptions, err error) {
|
func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo) (putOpts minio.PutObjectOptions, isMP bool, err error) {
|
||||||
meta := make(map[string]string)
|
meta := make(map[string]string)
|
||||||
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
isSSEC := crypto.SSEC.IsEncrypted(objInfo.UserDefined)
|
||||||
|
|
||||||
for k, v := range objInfo.UserDefined {
|
for k, v := range objInfo.UserDefined {
|
||||||
|
_, isValidSSEHeader := validSSEReplicationHeaders[k]
|
||||||
// In case of SSE-C objects copy the allowed internal headers as well
|
// In case of SSE-C objects copy the allowed internal headers as well
|
||||||
if !isSSEC || !slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
if !isSSEC || !isValidSSEHeader {
|
||||||
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
if stringsHasPrefixFold(k, ReservedMetadataPrefixLower) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -789,24 +789,28 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if slices.Contains(maps.Keys(validSSEReplicationHeaders), k) {
|
if isValidSSEHeader {
|
||||||
meta[validSSEReplicationHeaders[k]] = v
|
meta[validSSEReplicationHeaders[k]] = v
|
||||||
} else {
|
} else {
|
||||||
meta[k] = v
|
meta[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isMP = objInfo.isMultipart()
|
||||||
if len(objInfo.Checksum) > 0 {
|
if len(objInfo.Checksum) > 0 {
|
||||||
// Add encrypted CRC to metadata for SSE-C objects.
|
// Add encrypted CRC to metadata for SSE-C objects.
|
||||||
if isSSEC {
|
if isSSEC {
|
||||||
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
meta[ReplicationSsecChecksumHeader] = base64.StdEncoding.EncodeToString(objInfo.Checksum)
|
||||||
} else {
|
} else {
|
||||||
for _, pi := range objInfo.Parts {
|
cs, mp := getCRCMeta(objInfo, 0, nil)
|
||||||
if pi.Number == partNum {
|
// Set object checksum.
|
||||||
for k, v := range pi.Checksums {
|
for k, v := range cs {
|
||||||
meta[k] = v
|
meta[k] = v
|
||||||
}
|
}
|
||||||
}
|
isMP = mp
|
||||||
|
if !objInfo.isMultipart() && cs[xhttp.AmzChecksumType] == xhttp.AmzChecksumTypeFullObject {
|
||||||
|
// For objects where checksum is full object, it will be the same.
|
||||||
|
// Therefore, we use the cheaper PutObject replication.
|
||||||
|
isMP = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -837,7 +841,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
if tagTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
||||||
tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
|
tagTimestamp, err = time.Parse(time.RFC3339Nano, tagTmstampStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putOpts.Internal.TaggingTimestamp = tagTimestamp
|
putOpts.Internal.TaggingTimestamp = tagTimestamp
|
||||||
@@ -861,7 +865,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
if retainDateStr, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||||
rdate, err := amztime.ISO8601Parse(retainDateStr)
|
rdate, err := amztime.ISO8601Parse(retainDateStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, false, err
|
||||||
}
|
}
|
||||||
putOpts.RetainUntilDate = rdate
|
putOpts.RetainUntilDate = rdate
|
||||||
// set retention timestamp in opts
|
// set retention timestamp in opts
|
||||||
@@ -869,7 +873,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
if retainTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
||||||
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
|
retTimestamp, err = time.Parse(time.RFC3339Nano, retainTmstampStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putOpts.Internal.RetentionTimestamp = retTimestamp
|
putOpts.Internal.RetentionTimestamp = retTimestamp
|
||||||
@@ -881,7 +885,7 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
if lholdTmstampStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
||||||
lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
|
lholdTimestamp, err = time.Parse(time.RFC3339Nano, lholdTmstampStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return putOpts, err
|
return putOpts, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
putOpts.Internal.LegalholdTimestamp = lholdTimestamp
|
putOpts.Internal.LegalholdTimestamp = lholdTimestamp
|
||||||
@@ -889,6 +893,24 @@ func putReplicationOpts(ctx context.Context, sc string, objInfo ObjectInfo, part
|
|||||||
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
||||||
putOpts.ServerSideEncryption = encrypt.NewSSE()
|
putOpts.ServerSideEncryption = encrypt.NewSSE()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if crypto.S3KMS.IsEncrypted(objInfo.UserDefined) {
|
||||||
|
// If KMS key ID replication is enabled (as by default)
|
||||||
|
// we include the object's KMS key ID. In any case, we
|
||||||
|
// always set the SSE-KMS header. If no KMS key ID is
|
||||||
|
// specified, MinIO is supposed to use whatever default
|
||||||
|
// config applies on the site or bucket.
|
||||||
|
var keyID string
|
||||||
|
if kms.ReplicateKeyID() {
|
||||||
|
keyID = objInfo.KMSKeyID()
|
||||||
|
}
|
||||||
|
|
||||||
|
sseEnc, err := encrypt.NewSSEKMS(keyID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return putOpts, false, err
|
||||||
|
}
|
||||||
|
putOpts.ServerSideEncryption = sseEnc
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,7 +968,11 @@ func getReplicationAction(oi1 ObjectInfo, oi2 minio.ObjectInfo, opType replicati
|
|||||||
}
|
}
|
||||||
|
|
||||||
t, _ := tags.ParseObjectTags(oi1.UserTags)
|
t, _ := tags.ParseObjectTags(oi1.UserTags)
|
||||||
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2.UserTags, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
oi2Map := make(map[string]string)
|
||||||
|
for k, v := range oi2.UserTags {
|
||||||
|
oi2Map[k] = v
|
||||||
|
}
|
||||||
|
if (oi2.UserTagCount > 0 && !reflect.DeepEqual(oi2Map, t.ToMap())) || (oi2.UserTagCount != len(t.ToMap())) {
|
||||||
return replicateMetadata
|
return replicateMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1274,7 +1300,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
// use core client to avoid doing multipart on PUT
|
// use core client to avoid doing multipart on PUT
|
||||||
c := &minio.Core{Client: tgt.Client}
|
c := &minio.Core{Client: tgt.Client}
|
||||||
|
|
||||||
putOpts, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
replLogIf(ctx, fmt.Errorf("failure setting options for replication bucket:%s err:%w", bucket, err))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1306,7 +1332,7 @@ func (ri ReplicateObjectInfo) replicateObject(ctx context.Context, objectAPI Obj
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||||
if objInfo.isMultipart() {
|
if isMP {
|
||||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||||
} else {
|
} else {
|
||||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||||
@@ -1435,13 +1461,14 @@ func (ri ReplicateObjectInfo) replicateAll(ctx context.Context, objectAPI Object
|
|||||||
}
|
}
|
||||||
rinfo.Duration = time.Since(startTime)
|
rinfo.Duration = time.Since(startTime)
|
||||||
}()
|
}()
|
||||||
|
sOpts := minio.StatObjectOptions{
|
||||||
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, minio.StatObjectOptions{
|
|
||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
Internal: minio.AdvancedGetOptions{
|
Internal: minio.AdvancedGetOptions{
|
||||||
ReplicationProxyRequest: "false",
|
ReplicationProxyRequest: "false",
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
sOpts.Set(xhttp.AmzTagDirective, "ACCESS")
|
||||||
|
oi, cerr := tgt.StatObject(ctx, tgt.Bucket, object, sOpts)
|
||||||
if cerr == nil {
|
if cerr == nil {
|
||||||
rAction = getReplicationAction(objInfo, oi, ri.OpType)
|
rAction = getReplicationAction(objInfo, oi, ri.OpType)
|
||||||
rinfo.ReplicationStatus = replication.Completed
|
rinfo.ReplicationStatus = replication.Completed
|
||||||
@@ -1526,19 +1553,30 @@ applyAction:
|
|||||||
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
ReplicationRequest: true, // always set this to distinguish between `mc mirror` replication and serverside
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if tagTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+TaggingTimestamp]; ok {
|
// default timestamps to ModTime unless present in metadata
|
||||||
|
lkMap := caseInsensitiveMap(objInfo.UserDefined)
|
||||||
|
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockLegalHold); ok {
|
||||||
|
dstOpts.Internal.LegalholdTimestamp = objInfo.ModTime
|
||||||
|
}
|
||||||
|
if _, ok := lkMap.Lookup(xhttp.AmzObjectLockRetainUntilDate); ok {
|
||||||
|
dstOpts.Internal.RetentionTimestamp = objInfo.ModTime
|
||||||
|
}
|
||||||
|
if objInfo.UserTags != "" {
|
||||||
|
dstOpts.Internal.TaggingTimestamp = objInfo.ModTime
|
||||||
|
}
|
||||||
|
if tagTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + TaggingTimestamp); ok {
|
||||||
ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr)
|
ondiskTimestamp, err := time.Parse(time.RFC3339, tagTmStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
|
dstOpts.Internal.TaggingTimestamp = ondiskTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if retTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockRetentionTimestamp]; ok {
|
if retTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockRetentionTimestamp); ok {
|
||||||
ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
|
ondiskTimestamp, err := time.Parse(time.RFC3339, retTmStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
|
dstOpts.Internal.RetentionTimestamp = ondiskTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if lholdTmStr, ok := objInfo.UserDefined[ReservedMetadataPrefixLower+ObjectLockLegalHoldTimestamp]; ok {
|
if lholdTmStr, ok := lkMap.Lookup(ReservedMetadataPrefixLower + ObjectLockLegalHoldTimestamp); ok {
|
||||||
ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
|
ondiskTimestamp, err := time.Parse(time.RFC3339, lholdTmStr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
|
dstOpts.Internal.LegalholdTimestamp = ondiskTimestamp
|
||||||
@@ -1549,8 +1587,7 @@ applyAction:
|
|||||||
replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
|
replLogIf(ctx, fmt.Errorf("unable to replicate metadata for object %s/%s(%s) to target %s: %w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), rinfo.Err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var putOpts minio.PutObjectOptions
|
putOpts, isMP, err := putReplicationOpts(ctx, tgt.StorageClass, objInfo)
|
||||||
putOpts, err = putReplicationOpts(ctx, tgt.StorageClass, objInfo, 0)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
replLogIf(ctx, fmt.Errorf("failed to set replicate options for object %s/%s(%s) (target %s) err:%w", bucket, objInfo.Name, objInfo.VersionID, tgt.EndpointURL(), err))
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
@@ -1581,7 +1618,7 @@ applyAction:
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
}
|
}
|
||||||
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
r := bandwidth.NewMonitoredReader(newCtx, globalBucketMonitor, gr, opts)
|
||||||
if objInfo.isMultipart() {
|
if isMP {
|
||||||
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
rinfo.Err = replicateObjectWithMultipart(ctx, c, tgt.Bucket, object, r, objInfo, putOpts)
|
||||||
} else {
|
} else {
|
||||||
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
_, rinfo.Err = c.PutObject(ctx, tgt.Bucket, object, r, size, "", "", putOpts)
|
||||||
@@ -1659,7 +1696,8 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
cHeader := http.Header{}
|
cHeader := http.Header{}
|
||||||
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
cHeader.Add(xhttp.MinIOSourceReplicationRequest, "true")
|
||||||
if !isSSEC {
|
if !isSSEC {
|
||||||
for k, v := range partInfo.Checksums {
|
cs, _ := getCRCMeta(objInfo, partInfo.Number, nil)
|
||||||
|
for k, v := range cs {
|
||||||
cHeader.Add(k, v)
|
cHeader.Add(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1683,12 +1721,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size)
|
return fmt.Errorf("ssec(%t): Part size mismatch: got %d, want %d", isSSEC, pInfo.Size, size)
|
||||||
}
|
}
|
||||||
uploadedParts = append(uploadedParts, minio.CompletePart{
|
uploadedParts = append(uploadedParts, minio.CompletePart{
|
||||||
PartNumber: pInfo.PartNumber,
|
PartNumber: pInfo.PartNumber,
|
||||||
ETag: pInfo.ETag,
|
ETag: pInfo.ETag,
|
||||||
ChecksumCRC32: pInfo.ChecksumCRC32,
|
ChecksumCRC32: pInfo.ChecksumCRC32,
|
||||||
ChecksumCRC32C: pInfo.ChecksumCRC32C,
|
ChecksumCRC32C: pInfo.ChecksumCRC32C,
|
||||||
ChecksumSHA1: pInfo.ChecksumSHA1,
|
ChecksumSHA1: pInfo.ChecksumSHA1,
|
||||||
ChecksumSHA256: pInfo.ChecksumSHA256,
|
ChecksumSHA256: pInfo.ChecksumSHA256,
|
||||||
|
ChecksumCRC64NVME: pInfo.ChecksumCRC64NVME,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
userMeta := map[string]string{
|
userMeta := map[string]string{
|
||||||
@@ -1701,6 +1740,13 @@ func replicateObjectWithMultipart(ctx context.Context, c *minio.Core, bucket, ob
|
|||||||
// really big value but its okay on heavily loaded systems. This is just tail end timeout.
|
// really big value but its okay on heavily loaded systems. This is just tail end timeout.
|
||||||
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
|
cctx, ccancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||||
defer ccancel()
|
defer ccancel()
|
||||||
|
|
||||||
|
if len(objInfo.Checksum) > 0 {
|
||||||
|
cs, _ := getCRCMeta(objInfo, 0, nil)
|
||||||
|
for k, v := range cs {
|
||||||
|
userMeta[k] = strings.Split(v, "-")[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
_, err = c.CompleteMultipartUpload(cctx, bucket, object, uploadID, uploadedParts, minio.PutObjectOptions{
|
||||||
UserMetadata: userMeta,
|
UserMetadata: userMeta,
|
||||||
Internal: minio.AdvancedPutOptions{
|
Internal: minio.AdvancedPutOptions{
|
||||||
@@ -3317,7 +3363,7 @@ func getReplicationDiff(ctx context.Context, objAPI ObjectLayer, bucket string,
|
|||||||
}
|
}
|
||||||
for arn, st := range roi.TargetPurgeStatuses {
|
for arn, st := range roi.TargetPurgeStatuses {
|
||||||
if opts.ARN == "" || opts.ARN == arn {
|
if opts.ARN == "" || opts.ARN == arn {
|
||||||
if !opts.Verbose && st == Complete {
|
if !opts.Verbose && st == replication.VersionPurgeComplete {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t, ok := tgtsMap[arn]
|
t, ok := tgtsMap[arn]
|
||||||
@@ -3416,7 +3462,7 @@ func queueReplicationHeal(ctx context.Context, bucket string, oi ObjectInfo, rcf
|
|||||||
// heal delete marker replication failure or versioned delete replication failure
|
// heal delete marker replication failure or versioned delete replication failure
|
||||||
if roi.ReplicationStatus == replication.Pending ||
|
if roi.ReplicationStatus == replication.Pending ||
|
||||||
roi.ReplicationStatus == replication.Failed ||
|
roi.ReplicationStatus == replication.Failed ||
|
||||||
roi.VersionPurgeStatus == Failed || roi.VersionPurgeStatus == Pending {
|
roi.VersionPurgeStatus == replication.VersionPurgeFailed || roi.VersionPurgeStatus == replication.VersionPurgePending {
|
||||||
globalReplicationPool.Get().queueReplicaDeleteTask(dv)
|
globalReplicationPool.Get().queueReplicaDeleteTask(dv)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -3704,7 +3750,7 @@ func (p *ReplicationPool) queueMRFHeal() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *ReplicationPool) initialized() bool {
|
func (p *ReplicationPool) initialized() bool {
|
||||||
return !(p == nil || p.objLayer == nil)
|
return p != nil && p.objLayer != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMRF returns MRF entries for this node.
|
// getMRF returns MRF entries for this node.
|
||||||
@@ -3746,3 +3792,20 @@ type validateReplicationDestinationOptions struct {
|
|||||||
|
|
||||||
checkReadyErr sync.Map
|
checkReadyErr sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCRCMeta(oi ObjectInfo, partNum int, h http.Header) (cs map[string]string, isMP bool) {
|
||||||
|
meta := make(map[string]string)
|
||||||
|
cs, isMP = oi.decryptChecksums(partNum, h)
|
||||||
|
for k, v := range cs {
|
||||||
|
cksum := hash.NewChecksumString(k, v)
|
||||||
|
if cksum == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cksum.Valid() {
|
||||||
|
meta[cksum.Type.Key()] = v
|
||||||
|
meta[xhttp.AmzChecksumType] = cs[xhttp.AmzChecksumType]
|
||||||
|
meta[xhttp.AmzChecksumAlgo] = cksum.Type.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta, isMP
|
||||||
|
}
|
||||||
|
|||||||
+7
-7
@@ -108,18 +108,18 @@ func (l ReplicationLastHour) merge(o ReplicationLastHour) (merged ReplicationLas
|
|||||||
|
|
||||||
// Add a new duration data
|
// Add a new duration data
|
||||||
func (l *ReplicationLastHour) addsize(sz int64) {
|
func (l *ReplicationLastHour) addsize(sz int64) {
|
||||||
min := time.Now().Unix() / 60
|
minutes := time.Now().Unix() / 60
|
||||||
l.forwardTo(min)
|
l.forwardTo(minutes)
|
||||||
winIdx := min % 60
|
winIdx := minutes % 60
|
||||||
l.Totals[winIdx].merge(AccElem{Total: min, Size: sz, N: 1})
|
l.Totals[winIdx].merge(AccElem{Total: minutes, Size: sz, N: 1})
|
||||||
l.LastMin = min
|
l.LastMin = minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge all recorded counts of last hour into one
|
// Merge all recorded counts of last hour into one
|
||||||
func (l *ReplicationLastHour) getTotal() AccElem {
|
func (l *ReplicationLastHour) getTotal() AccElem {
|
||||||
var res AccElem
|
var res AccElem
|
||||||
min := time.Now().Unix() / 60
|
minutes := time.Now().Unix() / 60
|
||||||
l.forwardTo(min)
|
l.forwardTo(minutes)
|
||||||
for _, elem := range l.Totals[:] {
|
for _, elem := range l.Totals[:] {
|
||||||
res.merge(elem)
|
res.merge(elem)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-11
@@ -91,9 +91,6 @@ func init() {
|
|||||||
logger.Init(GOPATH, GOROOT)
|
logger.Init(GOPATH, GOROOT)
|
||||||
logger.RegisterError(config.FmtError)
|
logger.RegisterError(config.FmtError)
|
||||||
|
|
||||||
globalBatchJobsMetrics = batchJobMetrics{metrics: make(map[string]*batchJobInfo)}
|
|
||||||
go globalBatchJobsMetrics.purgeJobMetrics()
|
|
||||||
|
|
||||||
t, _ := minioVersionToReleaseTime(Version)
|
t, _ := minioVersionToReleaseTime(Version)
|
||||||
if !t.IsZero() {
|
if !t.IsZero() {
|
||||||
globalVersionUnix = uint64(t.Unix())
|
globalVersionUnix = uint64(t.Unix())
|
||||||
@@ -385,7 +382,7 @@ func buildServerCtxt(ctx *cli.Context, ctxt *serverCtxt) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check "no-compat" flag from command line argument.
|
// Check "no-compat" flag from command line argument.
|
||||||
ctxt.StrictS3Compat = !(ctx.IsSet("no-compat") || ctx.GlobalIsSet("no-compat"))
|
ctxt.StrictS3Compat = !ctx.IsSet("no-compat") && !ctx.GlobalIsSet("no-compat")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case ctx.IsSet("config-dir"):
|
case ctx.IsSet("config-dir"):
|
||||||
@@ -720,9 +717,7 @@ func serverHandleEnvVars() {
|
|||||||
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
|
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value in environment variable")
|
||||||
}
|
}
|
||||||
// Look for if URL has invalid values and return error.
|
// Look for if URL has invalid values and return error.
|
||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !isValidURLEndpoint((*url.URL)(u)) {
|
||||||
u.Opaque == "" &&
|
|
||||||
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
|
||||||
err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u)
|
err := fmt.Errorf("URL contains unexpected resources, expected URL to be one of http(s)://console.example.com or as a subpath via API endpoint http(s)://minio.example.com/minio format: %v", u)
|
||||||
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
|
logger.Fatal(err, "Invalid MINIO_BROWSER_REDIRECT_URL value is environment variable")
|
||||||
}
|
}
|
||||||
@@ -737,9 +732,7 @@ func serverHandleEnvVars() {
|
|||||||
logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
|
logger.Fatal(err, "Invalid MINIO_SERVER_URL value in environment variable")
|
||||||
}
|
}
|
||||||
// Look for if URL has invalid values and return error.
|
// Look for if URL has invalid values and return error.
|
||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !isValidURLEndpoint((*url.URL)(u)) {
|
||||||
(u.Path == "/" || u.Path == "") && u.Opaque == "" &&
|
|
||||||
!u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
|
||||||
err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
|
err := fmt.Errorf("URL contains unexpected resources, expected URL to be of http(s)://minio.example.com format: %v", u)
|
||||||
logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
|
logger.Fatal(err, "Invalid MINIO_SERVER_URL value is environment variable")
|
||||||
}
|
}
|
||||||
@@ -918,7 +911,7 @@ func handleKMSConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
||||||
if !(isFile(getPublicCertFile()) && isFile(getPrivateKeyFile())) {
|
if !isFile(getPublicCertFile()) || !isFile(getPrivateKeyFile()) {
|
||||||
return nil, nil, false, nil
|
return nil, nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func Test_readFromSecret(t *testing.T) {
|
|||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
testCase := testCase
|
testCase := testCase
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
tmpfile, err := os.CreateTemp("", "testfile")
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ MINIO_ROOT_PASSWORD=minio123`,
|
|||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
testCase := testCase
|
testCase := testCase
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
tmpfile, err := os.CreateTemp("", "testfile")
|
tmpfile, err := os.CreateTemp(t.TempDir(), "testfile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
|
"github.com/minio/madmin-go/v3/logger/log"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/minio/internal/logger/target/console"
|
"github.com/minio/minio/internal/logger/target/console"
|
||||||
"github.com/minio/minio/internal/logger/target/types"
|
"github.com/minio/minio/internal/logger/target/types"
|
||||||
"github.com/minio/minio/internal/pubsub"
|
"github.com/minio/minio/internal/pubsub"
|
||||||
"github.com/minio/pkg/v3/logger/message/log"
|
|
||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,22 @@ func (p *scannerMetrics) log(s scannerMetric, paths ...string) func(custom map[s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// time n scanner actions.
|
||||||
|
// Use for s < scannerMetricLastRealtime
|
||||||
|
func (p *scannerMetrics) timeN(s scannerMetric) func(n int) func() {
|
||||||
|
startTime := time.Now()
|
||||||
|
return func(n int) func() {
|
||||||
|
return func() {
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
|
||||||
|
atomic.AddUint64(&p.operations[s], uint64(n))
|
||||||
|
if s < scannerMetricLastRealtime {
|
||||||
|
p.latency[s].add(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// time a scanner action.
|
// time a scanner action.
|
||||||
// Use for s < scannerMetricLastRealtime
|
// Use for s < scannerMetricLastRealtime
|
||||||
func (p *scannerMetrics) time(s scannerMetric) func() {
|
func (p *scannerMetrics) time(s scannerMetric) func() {
|
||||||
|
|||||||
+130
-204
@@ -37,9 +37,7 @@ import (
|
|||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"github.com/minio/minio/internal/bucket/object/lock"
|
"github.com/minio/minio/internal/bucket/object/lock"
|
||||||
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
|
||||||
"github.com/minio/minio/internal/color"
|
"github.com/minio/minio/internal/color"
|
||||||
"github.com/minio/minio/internal/config/heal"
|
"github.com/minio/minio/internal/config/heal"
|
||||||
"github.com/minio/minio/internal/event"
|
"github.com/minio/minio/internal/event"
|
||||||
@@ -664,6 +662,12 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
into.addChild(h)
|
into.addChild(h)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Adjust the probability of healing.
|
||||||
|
// This first removes lowest x from the mod check and makes it x times more likely.
|
||||||
|
// So if duudc = 10 and we want heal check every 50 cycles, we check
|
||||||
|
// if (cycle/10) % (50/10) == 0, which would make heal checks run once every 50 cycles,
|
||||||
|
// if the objects are pre-selected as 1:10.
|
||||||
|
folder.objectHealProbDiv = dataUsageUpdateDirCycles
|
||||||
}
|
}
|
||||||
f.updateCurrentPath(folder.name)
|
f.updateCurrentPath(folder.name)
|
||||||
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
stopFn := globalScannerMetrics.log(scannerMetricScanFolder, f.root, folder.name)
|
||||||
@@ -858,8 +862,8 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if compact {
|
if compact {
|
||||||
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
stop := globalScannerMetrics.log(scannerMetricCompactFolder, folder.name)
|
||||||
f.newCache.deleteRecursive(thisHash)
|
f.newCache.deleteRecursive(thisHash)
|
||||||
@@ -873,7 +877,6 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int
|
|||||||
}
|
}
|
||||||
stop(total)
|
stop(total)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// Compact if too many children...
|
// Compact if too many children...
|
||||||
if !into.Compacted {
|
if !into.Compacted {
|
||||||
@@ -946,10 +949,7 @@ func (i *scannerItem) transformMetaDir() {
|
|||||||
i.objectName = split[len(split)-1]
|
i.objectName = split[len(split)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var applyActionsLogPrefix = color.Green("applyActions:")
|
||||||
applyActionsLogPrefix = color.Green("applyActions:")
|
|
||||||
applyVersionActionsLogPrefix = color.Green("applyVersionActions:")
|
|
||||||
)
|
|
||||||
|
|
||||||
func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
|
func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi ObjectInfo) (size int64) {
|
||||||
if i.debug {
|
if i.debug {
|
||||||
@@ -974,153 +974,8 @@ func (i *scannerItem) applyHealing(ctx context.Context, o ObjectLayer, oi Object
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *scannerItem) applyLifecycle(ctx context.Context, o ObjectLayer, oi ObjectInfo) (action lifecycle.Action, size int64) {
|
func (i *scannerItem) alertExcessiveVersions(remainingVersions int, cumulativeSize int64) {
|
||||||
size, err := oi.GetActualSize()
|
if remainingVersions >= int(scannerExcessObjectVersions.Load()) {
|
||||||
if i.debug {
|
|
||||||
scannerLogIf(ctx, err)
|
|
||||||
}
|
|
||||||
if i.lifeCycle == nil {
|
|
||||||
return action, size
|
|
||||||
}
|
|
||||||
|
|
||||||
versionID := oi.VersionID
|
|
||||||
|
|
||||||
var vc *versioning.Versioning
|
|
||||||
var lr objectlock.Retention
|
|
||||||
var rcfg *replication.Config
|
|
||||||
if !isMinioMetaBucketName(i.bucket) {
|
|
||||||
vc, err = globalBucketVersioningSys.Get(i.bucket)
|
|
||||||
if err != nil {
|
|
||||||
scannerLogOnceIf(ctx, err, i.bucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if bucket is object locked.
|
|
||||||
lr, err = globalBucketObjectLockSys.Get(i.bucket)
|
|
||||||
if err != nil {
|
|
||||||
scannerLogOnceIf(ctx, err, i.bucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rcfg, err = getReplicationConfig(ctx, i.bucket)
|
|
||||||
if err != nil {
|
|
||||||
scannerLogOnceIf(ctx, err, i.bucket)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lcEvt := evalActionFromLifecycle(ctx, *i.lifeCycle, lr, rcfg, oi)
|
|
||||||
if i.debug {
|
|
||||||
if versionID != "" {
|
|
||||||
console.Debugf(applyActionsLogPrefix+" lifecycle: %q (version-id=%s), Initial scan: %v\n", i.objectPath(), versionID, lcEvt.Action)
|
|
||||||
} else {
|
|
||||||
console.Debugf(applyActionsLogPrefix+" lifecycle: %q Initial scan: %v\n", i.objectPath(), lcEvt.Action)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch lcEvt.Action {
|
|
||||||
// This version doesn't contribute towards sizeS only when it is permanently deleted.
|
|
||||||
// This can happen when,
|
|
||||||
// - ExpireObjectAllVersions flag is enabled
|
|
||||||
// - NoncurrentVersionExpiration is applicable
|
|
||||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
|
|
||||||
size = 0
|
|
||||||
case lifecycle.DeleteAction:
|
|
||||||
// On a non-versioned bucket, DeleteObject removes the only version permanently.
|
|
||||||
if !vc.PrefixEnabled(oi.Name) {
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyLifecycleAction(lcEvt, lcEventSrc_Scanner, oi)
|
|
||||||
return lcEvt.Action, size
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyNewerNoncurrentVersionLimit removes noncurrent versions older than the most recent NewerNoncurrentVersions configured.
|
|
||||||
// Note: This function doesn't update sizeSummary since it always removes versions that it doesn't return.
|
|
||||||
func (i *scannerItem) applyNewerNoncurrentVersionLimit(ctx context.Context, _ ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
|
|
||||||
done := globalScannerMetrics.time(scannerMetricApplyNonCurrent)
|
|
||||||
defer done()
|
|
||||||
|
|
||||||
rcfg, _ := globalBucketObjectLockSys.Get(i.bucket)
|
|
||||||
vcfg, _ := globalBucketVersioningSys.Get(i.bucket)
|
|
||||||
|
|
||||||
versioned := vcfg != nil && vcfg.Versioned(i.objectPath())
|
|
||||||
|
|
||||||
objectInfos := make([]ObjectInfo, 0, len(fivs))
|
|
||||||
|
|
||||||
if i.lifeCycle == nil {
|
|
||||||
for _, fi := range fivs {
|
|
||||||
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
|
|
||||||
}
|
|
||||||
return objectInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
event := i.lifeCycle.NoncurrentVersionsExpirationLimit(lifecycle.ObjectOpts{Name: i.objectPath()})
|
|
||||||
lim := event.NewerNoncurrentVersions
|
|
||||||
if lim == 0 || len(fivs) <= lim+1 { // fewer than lim _noncurrent_ versions
|
|
||||||
for _, fi := range fivs {
|
|
||||||
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
|
|
||||||
}
|
|
||||||
return objectInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
overflowVersions := fivs[lim+1:]
|
|
||||||
// Retain the current version + most recent lim noncurrent versions
|
|
||||||
for _, fi := range fivs[:lim+1] {
|
|
||||||
objectInfos = append(objectInfos, fi.ToObjectInfo(i.bucket, i.objectPath(), versioned))
|
|
||||||
}
|
|
||||||
|
|
||||||
toDel := make([]ObjectToDelete, 0, len(overflowVersions))
|
|
||||||
for _, fi := range overflowVersions {
|
|
||||||
obj := fi.ToObjectInfo(i.bucket, i.objectPath(), versioned)
|
|
||||||
// skip versions with object locking enabled
|
|
||||||
if rcfg.LockEnabled && enforceRetentionForDeletion(ctx, obj) {
|
|
||||||
if i.debug {
|
|
||||||
if obj.VersionID != "" {
|
|
||||||
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s v(%s) is locked, not deleting\n", obj.Name, obj.VersionID)
|
|
||||||
} else {
|
|
||||||
console.Debugf(applyVersionActionsLogPrefix+" lifecycle: %s is locked, not deleting\n", obj.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// add this version back to remaining versions for
|
|
||||||
// subsequent lifecycle policy applications
|
|
||||||
objectInfos = append(objectInfos, obj)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoncurrentDays not passed yet.
|
|
||||||
if time.Now().UTC().Before(lifecycle.ExpectedExpiryTime(obj.SuccessorModTime, event.NoncurrentDays)) {
|
|
||||||
// add this version back to remaining versions for
|
|
||||||
// subsequent lifecycle policy applications
|
|
||||||
objectInfos = append(objectInfos, obj)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
toDel = append(toDel, ObjectToDelete{
|
|
||||||
ObjectV: ObjectV{
|
|
||||||
ObjectName: obj.Name,
|
|
||||||
VersionID: obj.VersionID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(toDel) > 0 {
|
|
||||||
expState.enqueueByNewerNoncurrent(i.bucket, toDel, event)
|
|
||||||
}
|
|
||||||
return objectInfos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyVersionActions will apply lifecycle checks on all versions of a scanned item. Returns versions that remain
|
|
||||||
// after applying lifecycle checks configured.
|
|
||||||
func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fivs []FileInfo, expState *expiryState) ([]ObjectInfo, error) {
|
|
||||||
objInfos, err := i.applyNewerNoncurrentVersionLimit(ctx, o, fivs, expState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we have many versions after applyNewerNoncurrentVersionLimit.
|
|
||||||
if len(objInfos) >= int(scannerExcessObjectVersions.Load()) {
|
|
||||||
// Notify object accessed via a GET request.
|
// Notify object accessed via a GET request.
|
||||||
sendEvent(eventArgs{
|
sendEvent(eventArgs{
|
||||||
EventName: event.ObjectManyVersions,
|
EventName: event.ObjectManyVersions,
|
||||||
@@ -1130,7 +985,7 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
|
|||||||
},
|
},
|
||||||
UserAgent: "Scanner",
|
UserAgent: "Scanner",
|
||||||
Host: globalLocalNodeName,
|
Host: globalLocalNodeName,
|
||||||
RespElements: map[string]string{"x-minio-versions": strconv.Itoa(len(objInfos))},
|
RespElements: map[string]string{"x-minio-versions": strconv.Itoa(remainingVersions)},
|
||||||
})
|
})
|
||||||
|
|
||||||
auditLogInternal(context.Background(), AuditLogOptions{
|
auditLogInternal(context.Background(), AuditLogOptions{
|
||||||
@@ -1139,15 +994,11 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
|
|||||||
Bucket: i.bucket,
|
Bucket: i.bucket,
|
||||||
Object: i.objectPath(),
|
Object: i.objectPath(),
|
||||||
Tags: map[string]string{
|
Tags: map[string]string{
|
||||||
"x-minio-versions": strconv.Itoa(len(objInfos)),
|
"x-minio-versions": strconv.Itoa(remainingVersions),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cumulativeSize := int64(0)
|
|
||||||
for _, objInfo := range objInfos {
|
|
||||||
cumulativeSize += objInfo.Size
|
|
||||||
}
|
|
||||||
// Check if the cumulative size of all versions of this object is high.
|
// Check if the cumulative size of all versions of this object is high.
|
||||||
if cumulativeSize >= scannerExcessObjectVersionsTotalSize.Load() {
|
if cumulativeSize >= scannerExcessObjectVersionsTotalSize.Load() {
|
||||||
// Notify object accessed via a GET request.
|
// Notify object accessed via a GET request.
|
||||||
@@ -1160,7 +1011,7 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
|
|||||||
UserAgent: "Scanner",
|
UserAgent: "Scanner",
|
||||||
Host: globalLocalNodeName,
|
Host: globalLocalNodeName,
|
||||||
RespElements: map[string]string{
|
RespElements: map[string]string{
|
||||||
"x-minio-versions-count": strconv.Itoa(len(objInfos)),
|
"x-minio-versions-count": strconv.Itoa(remainingVersions),
|
||||||
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
|
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1171,43 +1022,30 @@ func (i *scannerItem) applyVersionActions(ctx context.Context, o ObjectLayer, fi
|
|||||||
Bucket: i.bucket,
|
Bucket: i.bucket,
|
||||||
Object: i.objectPath(),
|
Object: i.objectPath(),
|
||||||
Tags: map[string]string{
|
Tags: map[string]string{
|
||||||
"x-minio-versions-count": strconv.Itoa(len(objInfos)),
|
"x-minio-versions-count": strconv.Itoa(remainingVersions),
|
||||||
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
|
"x-minio-versions-size": strconv.FormatInt(cumulativeSize, 10),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return objInfos, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type actionsAccountingFn func(oi ObjectInfo, sz, actualSz int64, sizeS *sizeSummary)
|
||||||
|
|
||||||
// applyActions will apply lifecycle checks on to a scanned item.
|
// applyActions will apply lifecycle checks on to a scanned item.
|
||||||
// The resulting size on disk will always be returned.
|
// The resulting size on disk will always be returned.
|
||||||
// The metadata will be compared to consensus on the object layer before any changes are applied.
|
// The metadata will be compared to consensus on the object layer before any changes are applied.
|
||||||
// If no metadata is supplied, -1 is returned if no action is taken.
|
// If no metadata is supplied, -1 is returned if no action is taken.
|
||||||
func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) (objDeleted bool, size int64) {
|
func (i *scannerItem) applyActions(ctx context.Context, objAPI ObjectLayer, objInfos []ObjectInfo, lr lock.Retention, sizeS *sizeSummary, fn actionsAccountingFn) {
|
||||||
done := globalScannerMetrics.time(scannerMetricILM)
|
healActions := func(oi ObjectInfo, actualSz int64) int64 {
|
||||||
var action lifecycle.Action
|
size := actualSz
|
||||||
action, size = i.applyLifecycle(ctx, o, oi)
|
|
||||||
done()
|
|
||||||
|
|
||||||
// Note: objDeleted is true if and only if action ==
|
|
||||||
// lifecycle.DeleteAllVersionsAction
|
|
||||||
if action.DeleteAll() {
|
|
||||||
return true, 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// For instance, an applied lifecycle means we remove/transitioned an object
|
|
||||||
// from the current deployment, which means we don't have to call healing
|
|
||||||
// routine even if we are asked to do via heal flag.
|
|
||||||
if action == lifecycle.NoneAction {
|
|
||||||
if i.heal.enabled {
|
if i.heal.enabled {
|
||||||
done := globalScannerMetrics.time(scannerMetricHealCheck)
|
done := globalScannerMetrics.time(scannerMetricHealCheck)
|
||||||
size = i.applyHealing(ctx, o, oi)
|
size = i.applyHealing(ctx, objAPI, oi)
|
||||||
done()
|
done()
|
||||||
|
|
||||||
if healDeleteDangling {
|
if healDeleteDangling {
|
||||||
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
|
done := globalScannerMetrics.time(scannerMetricCleanAbandoned)
|
||||||
err := o.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
|
err := objAPI.CheckAbandonedParts(ctx, i.bucket, i.objectPath(), madmin.HealOpts{Remove: healDeleteDangling})
|
||||||
done()
|
done()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath())
|
healingLogIf(ctx, fmt.Errorf("unable to check object %s/%s for abandoned data: %w", i.bucket, i.objectPath(), err), i.objectPath())
|
||||||
@@ -1217,10 +1055,109 @@ func (i *scannerItem) applyActions(ctx context.Context, o ObjectLayer, oi Object
|
|||||||
|
|
||||||
// replicate only if lifecycle rules are not applied.
|
// replicate only if lifecycle rules are not applied.
|
||||||
done := globalScannerMetrics.time(scannerMetricCheckReplication)
|
done := globalScannerMetrics.time(scannerMetricCheckReplication)
|
||||||
i.healReplication(ctx, o, oi.Clone(), sizeS)
|
i.healReplication(ctx, oi.Clone(), sizeS)
|
||||||
done()
|
done()
|
||||||
|
return size
|
||||||
}
|
}
|
||||||
return false, size
|
|
||||||
|
vc, err := globalBucketVersioningSys.Get(i.bucket)
|
||||||
|
if err != nil {
|
||||||
|
scannerLogOnceIf(ctx, err, i.bucket)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start ILM check timer
|
||||||
|
done := globalScannerMetrics.timeN(scannerMetricILM)
|
||||||
|
if i.lifeCycle == nil { // no ILM configured, apply healing and replication checks
|
||||||
|
var cumulativeSize int64
|
||||||
|
for _, oi := range objInfos {
|
||||||
|
actualSz, err := oi.GetActualSize()
|
||||||
|
if err != nil {
|
||||||
|
scannerLogIf(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size := healActions(oi, actualSz)
|
||||||
|
if fn != nil { // call accountingfn
|
||||||
|
fn(oi, size, actualSz, sizeS)
|
||||||
|
}
|
||||||
|
cumulativeSize += size
|
||||||
|
}
|
||||||
|
// end ILM check timer
|
||||||
|
done(len(objInfos))
|
||||||
|
i.alertExcessiveVersions(len(objInfos), cumulativeSize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objOpts := make([]lifecycle.ObjectOpts, len(objInfos))
|
||||||
|
for i, oi := range objInfos {
|
||||||
|
objOpts[i] = oi.ToLifecycleOpts()
|
||||||
|
}
|
||||||
|
evaluator := lifecycle.NewEvaluator(*i.lifeCycle).WithLockRetention(&lr).WithReplicationConfig(i.replication.Config)
|
||||||
|
events, err := evaluator.Eval(objOpts)
|
||||||
|
if err != nil {
|
||||||
|
// This error indicates that the objOpts passed to Eval is invalid.
|
||||||
|
bugLogIf(ctx, err, i.bucket, i.objectPath())
|
||||||
|
done(len(objInfos)) // end ILM check timer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done(len(objInfos)) // end ILM check timer
|
||||||
|
|
||||||
|
var (
|
||||||
|
toDel []ObjectToDelete
|
||||||
|
noncurrentEvents []lifecycle.Event
|
||||||
|
cumulativeSize int64
|
||||||
|
)
|
||||||
|
remainingVersions := len(objInfos)
|
||||||
|
eventLoop:
|
||||||
|
for idx, event := range events {
|
||||||
|
oi := objInfos[idx]
|
||||||
|
actualSz, err := oi.GetActualSize()
|
||||||
|
if i.debug {
|
||||||
|
scannerLogIf(ctx, err)
|
||||||
|
}
|
||||||
|
size := actualSz
|
||||||
|
switch event.Action {
|
||||||
|
case lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
|
||||||
|
remainingVersions = 0
|
||||||
|
applyExpiryRule(event, lcEventSrc_Scanner, oi)
|
||||||
|
break eventLoop
|
||||||
|
|
||||||
|
case lifecycle.DeleteAction, lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction:
|
||||||
|
if !vc.PrefixEnabled(i.objectPath()) && event.Action == lifecycle.DeleteAction {
|
||||||
|
remainingVersions--
|
||||||
|
size = 0
|
||||||
|
}
|
||||||
|
applyExpiryRule(event, lcEventSrc_Scanner, oi)
|
||||||
|
|
||||||
|
case lifecycle.DeleteVersionAction: // noncurrent versions expiration
|
||||||
|
opts := objOpts[idx]
|
||||||
|
remainingVersions--
|
||||||
|
size = 0
|
||||||
|
toDel = append(toDel, ObjectToDelete{
|
||||||
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: opts.Name,
|
||||||
|
VersionID: opts.VersionID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
noncurrentEvents = append(noncurrentEvents, event)
|
||||||
|
|
||||||
|
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
||||||
|
applyTransitionRule(event, lcEventSrc_Scanner, oi)
|
||||||
|
|
||||||
|
case lifecycle.NoneAction:
|
||||||
|
size = healActions(oi, actualSz)
|
||||||
|
}
|
||||||
|
// NB fn must be called for every object version except if it is
|
||||||
|
// expired or was a dangling object.
|
||||||
|
if fn != nil {
|
||||||
|
fn(oi, size, actualSz, sizeS)
|
||||||
|
}
|
||||||
|
cumulativeSize += size
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toDel) > 0 {
|
||||||
|
globalExpiryState.enqueueNoncurrentVersions(i.bucket, toDel, noncurrentEvents)
|
||||||
|
}
|
||||||
|
i.alertExcessiveVersions(remainingVersions, cumulativeSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event {
|
func evalActionFromLifecycle(ctx context.Context, lc lifecycle.Lifecycle, lr lock.Retention, rcfg *replication.Config, obj ObjectInfo) lifecycle.Event {
|
||||||
@@ -1325,10 +1262,13 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
|
dobj, err = objLayer.DeleteObject(ctx, obj.Bucket, encodeDirObject(obj.Name), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||||
|
traceFn(ILMExpiry, nil, nil)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Assume it is still there.
|
// Assume it is still there.
|
||||||
ilmLogOnceIf(ctx, fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err), "non-transition-expiry"+obj.Name)
|
err := fmt.Errorf("DeleteObject(%s, %s): %w", obj.Bucket, obj.Name, err)
|
||||||
|
ilmLogOnceIf(ctx, err, "non-transition-expiry"+obj.Name)
|
||||||
|
traceFn(ILMExpiry, nil, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if dobj.Name == "" {
|
if dobj.Name == "" {
|
||||||
@@ -1364,22 +1304,8 @@ func applyExpiryOnNonTransitionedObjects(ctx context.Context, objLayer ObjectLay
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply object, object version, restored object or restored object version action on the given object
|
// Apply object, object version, restored object or restored object version action on the given object
|
||||||
func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) bool {
|
func applyExpiryRule(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) {
|
||||||
globalExpiryState.enqueueByDays(obj, event, src)
|
globalExpiryState.enqueueByDays(obj, event, src)
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform actions (removal or transitioning of objects), return true the action is successfully performed
|
|
||||||
func applyLifecycleAction(event lifecycle.Event, src lcEventSrc, obj ObjectInfo) (success bool) {
|
|
||||||
switch action := event.Action; action {
|
|
||||||
case lifecycle.DeleteVersionAction, lifecycle.DeleteAction,
|
|
||||||
lifecycle.DeleteRestoredAction, lifecycle.DeleteRestoredVersionAction,
|
|
||||||
lifecycle.DeleteAllVersionsAction, lifecycle.DelMarkerDeleteAllVersionsAction:
|
|
||||||
success = applyExpiryRule(event, src, obj)
|
|
||||||
case lifecycle.TransitionAction, lifecycle.TransitionVersionAction:
|
|
||||||
success = applyTransitionRule(event, src, obj)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectPath returns the prefix and object name.
|
// objectPath returns the prefix and object name.
|
||||||
@@ -1388,7 +1314,7 @@ func (i *scannerItem) objectPath() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// healReplication will heal a scanned item that has failed replication.
|
// healReplication will heal a scanned item that has failed replication.
|
||||||
func (i *scannerItem) healReplication(ctx context.Context, o ObjectLayer, oi ObjectInfo, sizeS *sizeSummary) {
|
func (i *scannerItem) healReplication(ctx context.Context, oi ObjectInfo, sizeS *sizeSummary) {
|
||||||
if oi.VersionID == "" {
|
if oi.VersionID == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1549,7 +1475,7 @@ const (
|
|||||||
ILMTransition = " ilm:transition"
|
ILMTransition = " ilm:transition"
|
||||||
)
|
)
|
||||||
|
|
||||||
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string)) {
|
func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags map[string]string, traceFn func(event string, metadata map[string]string, err error)) {
|
||||||
var apiName string
|
var apiName string
|
||||||
switch event {
|
switch event {
|
||||||
case ILMExpiry:
|
case ILMExpiry:
|
||||||
@@ -1567,5 +1493,5 @@ func auditLogLifecycle(ctx context.Context, oi ObjectInfo, event string, tags ma
|
|||||||
VersionID: oi.VersionID,
|
VersionID: oi.VersionID,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
})
|
})
|
||||||
traceFn(event, tags)
|
traceFn(event, tags, nil)
|
||||||
}
|
}
|
||||||
|
|||||||
+231
-68
@@ -21,58 +21,65 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio/internal/amztime"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"github.com/minio/minio/internal/bucket/object/lock"
|
objectlock "github.com/minio/minio/internal/bucket/object/lock"
|
||||||
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
|
func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
|
||||||
|
// Prepare object layer
|
||||||
objAPI, disks, err := prepareErasure(context.Background(), 8)
|
objAPI, disks, err := prepareErasure(context.Background(), 8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to initialize object layer: %v", err)
|
t.Fatalf("Failed to initialize object layer: %v", err)
|
||||||
}
|
}
|
||||||
defer removeRoots(disks)
|
defer removeRoots(disks)
|
||||||
setObjectLayer(objAPI)
|
setObjectLayer(objAPI)
|
||||||
|
|
||||||
|
// Prepare bucket metadata
|
||||||
globalBucketMetadataSys = NewBucketMetadataSys()
|
globalBucketMetadataSys = NewBucketMetadataSys()
|
||||||
globalBucketObjectLockSys = &BucketObjectLockSys{}
|
globalBucketObjectLockSys = &BucketObjectLockSys{}
|
||||||
globalBucketVersioningSys = &BucketVersioningSys{}
|
globalBucketVersioningSys = &BucketVersioningSys{}
|
||||||
es := newExpiryState(context.Background(), objAPI, 0)
|
|
||||||
workers := []chan expiryOp{make(chan expiryOp)}
|
lcXML := `
|
||||||
es.workers.Store(&workers)
|
<LifecycleConfiguration>
|
||||||
globalExpiryState = es
|
<Rule>
|
||||||
var wg sync.WaitGroup
|
<ID>max-versions</ID>
|
||||||
wg.Add(1)
|
<Status>Enabled</Status>
|
||||||
expired := make([]ObjectToDelete, 0, 5)
|
<NoncurrentVersionExpiration>
|
||||||
go func() {
|
<NewerNoncurrentVersions>2</NewerNoncurrentVersions>
|
||||||
defer wg.Done()
|
</NoncurrentVersionExpiration>
|
||||||
workers := globalExpiryState.workers.Load()
|
</Rule>
|
||||||
for t := range (*workers)[0] {
|
<Rule>
|
||||||
if t, ok := t.(newerNoncurrentTask); ok {
|
<ID>delete-all-versions</ID>
|
||||||
expired = append(expired, t.versions...)
|
<Status>Enabled</Status>
|
||||||
}
|
<Filter>
|
||||||
}
|
<Tag>
|
||||||
}()
|
<Key>del-all</Key>
|
||||||
lc := lifecycle.Lifecycle{
|
<Value>true</Value>
|
||||||
Rules: []lifecycle.Rule{
|
</Tag>
|
||||||
{
|
</Filter>
|
||||||
ID: "max-versions",
|
<Expiration>
|
||||||
Status: "Enabled",
|
<Days>1</Days>
|
||||||
NoncurrentVersionExpiration: lifecycle.NoncurrentVersionExpiration{
|
<ExpiredObjectAllVersions>true</ExpiredObjectAllVersions>
|
||||||
NewerNoncurrentVersions: 1,
|
</Expiration>
|
||||||
},
|
</Rule>
|
||||||
},
|
</LifecycleConfiguration>
|
||||||
},
|
`
|
||||||
}
|
lc, err := lifecycle.ParseLifecycleConfig(strings.NewReader(lcXML))
|
||||||
lcXML, err := xml.Marshal(lc)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to marshal lifecycle config: %v", err)
|
t.Fatalf("Failed to unmarshal lifecycle config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vcfg := versioning.Versioning{
|
vcfg := versioning.Versioning{
|
||||||
Status: "Enabled",
|
Status: "Enabled",
|
||||||
}
|
}
|
||||||
@@ -82,33 +89,45 @@ func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bucket := "bucket"
|
bucket := "bucket"
|
||||||
obj := "obj-1"
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
meta := BucketMetadata{
|
meta := BucketMetadata{
|
||||||
Name: bucket,
|
Name: bucket,
|
||||||
Created: now,
|
Created: now,
|
||||||
LifecycleConfigXML: lcXML,
|
LifecycleConfigXML: []byte(lcXML),
|
||||||
VersioningConfigXML: vcfgXML,
|
VersioningConfigXML: vcfgXML,
|
||||||
VersioningConfigUpdatedAt: now,
|
VersioningConfigUpdatedAt: now,
|
||||||
LifecycleConfigUpdatedAt: now,
|
LifecycleConfigUpdatedAt: now,
|
||||||
lifecycleConfig: &lc,
|
lifecycleConfig: lc,
|
||||||
versioningConfig: &vcfg,
|
versioningConfig: &vcfg,
|
||||||
}
|
}
|
||||||
globalBucketMetadataSys.Set(bucket, meta)
|
globalBucketMetadataSys.Set(bucket, meta)
|
||||||
item := scannerItem{
|
// Prepare lifecycle expiration workers
|
||||||
Path: obj,
|
es := newExpiryState(context.Background(), objAPI, 0)
|
||||||
bucket: bucket,
|
globalExpiryState = es
|
||||||
prefix: "",
|
|
||||||
objectName: obj,
|
|
||||||
lifeCycle: &lc,
|
|
||||||
}
|
|
||||||
|
|
||||||
modTime := time.Now()
|
// Prepare object versions
|
||||||
|
obj := "obj-1"
|
||||||
|
// Simulate objects uploaded 30 hours ago
|
||||||
|
modTime := now.Add(-48 * time.Hour)
|
||||||
uuids := make([]uuid.UUID, 5)
|
uuids := make([]uuid.UUID, 5)
|
||||||
for i := range uuids {
|
for i := range uuids {
|
||||||
uuids[i] = uuid.UUID([16]byte{15: uint8(i + 1)})
|
uuids[i] = uuid.UUID([16]byte{15: uint8(i + 1)})
|
||||||
}
|
}
|
||||||
fivs := make([]FileInfo, 5)
|
fivs := make([]FileInfo, 5)
|
||||||
|
objInfos := make([]ObjectInfo, 5)
|
||||||
|
objRetentionMeta := make(map[string]string)
|
||||||
|
objRetentionMeta[strings.ToLower(xhttp.AmzObjectLockMode)] = string(objectlock.RetCompliance)
|
||||||
|
// Set retain until date 12 hours into the future
|
||||||
|
objRetentionMeta[strings.ToLower(xhttp.AmzObjectLockRetainUntilDate)] = amztime.ISO8601Format(now.Add(12 * time.Hour))
|
||||||
|
/*
|
||||||
|
objInfos:
|
||||||
|
version stack for obj-1
|
||||||
|
v5 uuid-5 modTime
|
||||||
|
v4 uuid-4 modTime -1m
|
||||||
|
v3 uuid-3 modTime -2m
|
||||||
|
v2 uuid-2 modTime -3m
|
||||||
|
v1 uuid-1 modTime -4m
|
||||||
|
*/
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
fivs[i] = FileInfo{
|
fivs[i] = FileInfo{
|
||||||
Volume: bucket,
|
Volume: bucket,
|
||||||
@@ -119,41 +138,183 @@ func TestApplyNewerNoncurrentVersionsLimit(t *testing.T) {
|
|||||||
Size: 1 << 10,
|
Size: 1 << 10,
|
||||||
NumVersions: 5,
|
NumVersions: 5,
|
||||||
}
|
}
|
||||||
|
objInfos[i] = fivs[i].ToObjectInfo(bucket, obj, true)
|
||||||
}
|
}
|
||||||
versioned := vcfg.Status == "Enabled"
|
/*
|
||||||
wants := make([]ObjectInfo, 2)
|
lrObjInfos: objInfos with following modifications
|
||||||
for i, fi := range fivs[:2] {
|
version stack for obj-1
|
||||||
wants[i] = fi.ToObjectInfo(bucket, obj, versioned)
|
v2 uuid-2 modTime -3m objRetentionMeta
|
||||||
|
*/
|
||||||
|
lrObjInfos := slices.Clone(objInfos)
|
||||||
|
lrObjInfos[3].UserDefined = objRetentionMeta
|
||||||
|
var lrWants []ObjectInfo
|
||||||
|
lrWants = append(lrWants, lrObjInfos[:4]...)
|
||||||
|
|
||||||
|
/*
|
||||||
|
replObjInfos: objInfos with following modifications
|
||||||
|
version stack for obj-1
|
||||||
|
v1 uuid-1 modTime -4m "VersionPurgeStatus: replication.VersionPurgePending"
|
||||||
|
*/
|
||||||
|
replObjInfos := slices.Clone(objInfos)
|
||||||
|
replObjInfos[4].VersionPurgeStatus = replication.VersionPurgePending
|
||||||
|
var replWants []ObjectInfo
|
||||||
|
replWants = append(replWants, replObjInfos[:3]...)
|
||||||
|
replWants = append(replWants, replObjInfos[4])
|
||||||
|
|
||||||
|
allVersExpObjInfos := slices.Clone(objInfos)
|
||||||
|
allVersExpObjInfos[0].UserTags = "del-all=true"
|
||||||
|
|
||||||
|
replCfg := replication.Config{
|
||||||
|
Rules: []replication.Rule{
|
||||||
|
{
|
||||||
|
ID: "",
|
||||||
|
Status: "Enabled",
|
||||||
|
Priority: 1,
|
||||||
|
Destination: replication.Destination{
|
||||||
|
ARN: "arn:minio:replication:::dest-bucket",
|
||||||
|
Bucket: "dest-bucket",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
gots, err := item.applyNewerNoncurrentVersionLimit(context.TODO(), objAPI, fivs, es)
|
lr := objectlock.Retention{
|
||||||
if err != nil {
|
Mode: objectlock.RetCompliance,
|
||||||
t.Fatalf("Failed with err: %v", err)
|
Validity: 12 * time.Hour,
|
||||||
}
|
LockEnabled: true,
|
||||||
if len(gots) != len(wants) {
|
|
||||||
t.Fatalf("Expected %d objects but got %d", len(wants), len(gots))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close expiry state's channel to inspect object versions enqueued for expiration
|
expiryWorker := func(wg *sync.WaitGroup, readyCh chan<- struct{}, taskCh <-chan expiryOp, gotExpired *[]ObjectToDelete) {
|
||||||
close(workers[0])
|
defer wg.Done()
|
||||||
wg.Wait()
|
// signal the calling goroutine that the worker is ready tor receive tasks
|
||||||
for _, obj := range expired {
|
close(readyCh)
|
||||||
switch obj.ObjectV.VersionID {
|
var expired []ObjectToDelete
|
||||||
case uuids[2].String(), uuids[3].String(), uuids[4].String():
|
for t := range taskCh {
|
||||||
default:
|
switch v := t.(type) {
|
||||||
t.Errorf("Unexpected versionID being expired: %#v\n", obj)
|
case noncurrentVersionsTask:
|
||||||
|
expired = append(expired, v.versions...)
|
||||||
|
case expiryTask:
|
||||||
|
expired = append(expired, ObjectToDelete{
|
||||||
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: v.objInfo.Name,
|
||||||
|
VersionID: v.objInfo.VersionID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if len(expired) > 0 {
|
||||||
|
*gotExpired = expired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
replCfg replicationConfig
|
||||||
|
lr objectlock.Retention
|
||||||
|
objInfos []ObjectInfo
|
||||||
|
wants []ObjectInfo
|
||||||
|
wantExpired []ObjectToDelete
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// With replication configured, version(s) with PENDING purge status
|
||||||
|
replCfg: replicationConfig{Config: &replCfg},
|
||||||
|
objInfos: replObjInfos,
|
||||||
|
wants: replWants,
|
||||||
|
wantExpired: []ObjectToDelete{
|
||||||
|
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[3].VersionID}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// With lock retention configured and version(s) with retention metadata
|
||||||
|
lr: lr,
|
||||||
|
objInfos: lrObjInfos,
|
||||||
|
wants: lrWants,
|
||||||
|
wantExpired: []ObjectToDelete{
|
||||||
|
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[4].VersionID}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// With replication configured, but no versions with PENDING purge status
|
||||||
|
replCfg: replicationConfig{Config: &replCfg},
|
||||||
|
objInfos: objInfos,
|
||||||
|
wants: objInfos[:3],
|
||||||
|
wantExpired: []ObjectToDelete{
|
||||||
|
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[3].VersionID}},
|
||||||
|
{ObjectV: ObjectV{ObjectName: obj, VersionID: objInfos[4].VersionID}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objInfos: allVersExpObjInfos,
|
||||||
|
wants: nil,
|
||||||
|
wantExpired: []ObjectToDelete{{ObjectV: ObjectV{ObjectName: obj, VersionID: allVersExpObjInfos[0].VersionID}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
t.Run(fmt.Sprintf("TestApplyNewerNoncurrentVersionsLimit-%d", i), func(t *testing.T) {
|
||||||
|
workers := []chan expiryOp{make(chan expiryOp)}
|
||||||
|
es.workers.Store(&workers)
|
||||||
|
workerReady := make(chan struct{})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
var gotExpired []ObjectToDelete
|
||||||
|
go expiryWorker(&wg, workerReady, workers[0], &gotExpired)
|
||||||
|
<-workerReady
|
||||||
|
|
||||||
|
item := scannerItem{
|
||||||
|
Path: obj,
|
||||||
|
bucket: bucket,
|
||||||
|
prefix: "",
|
||||||
|
objectName: obj,
|
||||||
|
lifeCycle: lc,
|
||||||
|
replication: test.replCfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sizeS sizeSummary
|
||||||
|
gots []ObjectInfo
|
||||||
|
)
|
||||||
|
item.applyActions(context.TODO(), objAPI, test.objInfos, test.lr, &sizeS, func(oi ObjectInfo, sz, _ int64, _ *sizeSummary) {
|
||||||
|
if sz != 0 {
|
||||||
|
gots = append(gots, oi)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(gots) != len(test.wants) {
|
||||||
|
t.Fatalf("Expected %d objects but got %d", len(test.wants), len(gots))
|
||||||
|
}
|
||||||
|
if slices.CompareFunc(gots, test.wants, func(g, w ObjectInfo) int {
|
||||||
|
if g.VersionID == w.VersionID {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}) != 0 {
|
||||||
|
t.Fatalf("Expected %v but got %v", test.wants, gots)
|
||||||
|
}
|
||||||
|
// verify the objects to be deleted
|
||||||
|
close(workers[0])
|
||||||
|
wg.Wait()
|
||||||
|
if len(gotExpired) != len(test.wantExpired) {
|
||||||
|
t.Fatalf("Expected expiry of %d objects but got %d", len(test.wantExpired), len(gotExpired))
|
||||||
|
}
|
||||||
|
if slices.CompareFunc(gotExpired, test.wantExpired, func(g, w ObjectToDelete) int {
|
||||||
|
if g.VersionID == w.VersionID {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}) != 0 {
|
||||||
|
t.Fatalf("Expected %v but got %v", test.wantExpired, gotExpired)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEvalActionFromLifecycle(t *testing.T) {
|
func TestEvalActionFromLifecycle(t *testing.T) {
|
||||||
// Tests cover only ExpiredObjectDeleteAllVersions and DelMarkerExpiration actions
|
// Tests cover only ExpiredObjectDeleteAllVersions and DelMarkerExpiration actions
|
||||||
|
numVersions := 4
|
||||||
obj := ObjectInfo{
|
obj := ObjectInfo{
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
ModTime: time.Now().Add(-31 * 24 * time.Hour),
|
ModTime: time.Now().Add(-31 * 24 * time.Hour),
|
||||||
Size: 100 << 20,
|
Size: 100 << 20,
|
||||||
VersionID: uuid.New().String(),
|
VersionID: uuid.New().String(),
|
||||||
IsLatest: true,
|
IsLatest: true,
|
||||||
NumVersions: 4,
|
NumVersions: numVersions,
|
||||||
}
|
}
|
||||||
delMarker := ObjectInfo{
|
delMarker := ObjectInfo{
|
||||||
Name: "foo-deleted",
|
Name: "foo-deleted",
|
||||||
@@ -162,8 +323,9 @@ func TestEvalActionFromLifecycle(t *testing.T) {
|
|||||||
VersionID: uuid.New().String(),
|
VersionID: uuid.New().String(),
|
||||||
IsLatest: true,
|
IsLatest: true,
|
||||||
DeleteMarker: true,
|
DeleteMarker: true,
|
||||||
NumVersions: 4,
|
NumVersions: numVersions,
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAllILM := `<LifecycleConfiguration>
|
deleteAllILM := `<LifecycleConfiguration>
|
||||||
<Rule>
|
<Rule>
|
||||||
<Expiration>
|
<Expiration>
|
||||||
@@ -195,35 +357,35 @@ func TestEvalActionFromLifecycle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
ilm lifecycle.Lifecycle
|
ilm lifecycle.Lifecycle
|
||||||
retention lock.Retention
|
retention *objectlock.Retention
|
||||||
obj ObjectInfo
|
obj ObjectInfo
|
||||||
want lifecycle.Action
|
want lifecycle.Action
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
// with object locking
|
// with object locking
|
||||||
ilm: *deleteAllLc,
|
ilm: *deleteAllLc,
|
||||||
retention: lock.Retention{LockEnabled: true},
|
retention: &objectlock.Retention{LockEnabled: true},
|
||||||
obj: obj,
|
obj: obj,
|
||||||
want: lifecycle.NoneAction,
|
want: lifecycle.NoneAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// without object locking
|
// without object locking
|
||||||
ilm: *deleteAllLc,
|
ilm: *deleteAllLc,
|
||||||
retention: lock.Retention{},
|
retention: &objectlock.Retention{},
|
||||||
obj: obj,
|
obj: obj,
|
||||||
want: lifecycle.DeleteAllVersionsAction,
|
want: lifecycle.DeleteAllVersionsAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// with object locking
|
// with object locking
|
||||||
ilm: *delMarkerLc,
|
ilm: *delMarkerLc,
|
||||||
retention: lock.Retention{LockEnabled: true},
|
retention: &objectlock.Retention{LockEnabled: true},
|
||||||
obj: delMarker,
|
obj: delMarker,
|
||||||
want: lifecycle.NoneAction,
|
want: lifecycle.NoneAction,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// without object locking
|
// without object locking
|
||||||
ilm: *delMarkerLc,
|
ilm: *delMarkerLc,
|
||||||
retention: lock.Retention{},
|
retention: &objectlock.Retention{},
|
||||||
obj: delMarker,
|
obj: delMarker,
|
||||||
want: lifecycle.DelMarkerDeleteAllVersionsAction,
|
want: lifecycle.DelMarkerDeleteAllVersionsAction,
|
||||||
},
|
},
|
||||||
@@ -231,8 +393,9 @@ func TestEvalActionFromLifecycle(t *testing.T) {
|
|||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
t.Run(fmt.Sprintf("TestEvalAction-%d", i), func(t *testing.T) {
|
t.Run(fmt.Sprintf("TestEvalAction-%d", i), func(t *testing.T) {
|
||||||
if got := evalActionFromLifecycle(context.TODO(), test.ilm, test.retention, nil, test.obj); got.Action != test.want {
|
gotEvent := evalActionFromLifecycle(context.Background(), test.ilm, *test.retention, nil, test.obj)
|
||||||
t.Fatalf("Expected %v but got %v", test.want, got)
|
if gotEvent.Action != test.want {
|
||||||
|
t.Fatalf("Expected %v but got %v", test.want, gotEvent.Action)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-284
@@ -39,6 +39,8 @@ import (
|
|||||||
"github.com/valyala/bytebufferpool"
|
"github.com/valyala/bytebufferpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//msgp:clearomitted
|
||||||
|
|
||||||
//go:generate msgp -file $GOFILE -unexported
|
//go:generate msgp -file $GOFILE -unexported
|
||||||
|
|
||||||
// dataUsageHash is the hash type used.
|
// dataUsageHash is the hash type used.
|
||||||
@@ -57,15 +59,14 @@ type versionsHistogram [dataUsageVersionLen]uint64
|
|||||||
type dataUsageEntry struct {
|
type dataUsageEntry struct {
|
||||||
Children dataUsageHashMap `msg:"ch"`
|
Children dataUsageHashMap `msg:"ch"`
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64 `msg:"sz"`
|
Size int64 `msg:"sz"`
|
||||||
Objects uint64 `msg:"os"`
|
Objects uint64 `msg:"os"`
|
||||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||||
DeleteMarkers uint64 `msg:"dms"`
|
DeleteMarkers uint64 `msg:"dms"`
|
||||||
ObjSizes sizeHistogram `msg:"szs"`
|
ObjSizes sizeHistogram `msg:"szs"`
|
||||||
ObjVersions versionsHistogram `msg:"vh"`
|
ObjVersions versionsHistogram `msg:"vh"`
|
||||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
Compacted bool `msg:"c"`
|
||||||
Compacted bool `msg:"c"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allTierStats is a collection of per-tier stats across all configured remote
|
// allTierStats is a collection of per-tier stats across all configured remote
|
||||||
@@ -117,7 +118,6 @@ func (ats *allTierStats) populateStats(stats map[string]madmin.TierStats) {
|
|||||||
NumObjects: st.NumObjects,
|
NumObjects: st.NumObjects,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tierStats holds per-tier stats of a remote tier.
|
// tierStats holds per-tier stats of a remote tier.
|
||||||
@@ -135,93 +135,6 @@ func (ts tierStats) add(u tierStats) tierStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//msgp:tuple replicationStatsV1
|
|
||||||
type replicationStatsV1 struct {
|
|
||||||
PendingSize uint64
|
|
||||||
ReplicatedSize uint64
|
|
||||||
FailedSize uint64
|
|
||||||
ReplicaSize uint64
|
|
||||||
FailedCount uint64
|
|
||||||
PendingCount uint64
|
|
||||||
MissedThresholdSize uint64
|
|
||||||
AfterThresholdSize uint64
|
|
||||||
MissedThresholdCount uint64
|
|
||||||
AfterThresholdCount uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rsv1 replicationStatsV1) Empty() bool {
|
|
||||||
return rsv1.ReplicatedSize == 0 &&
|
|
||||||
rsv1.FailedSize == 0 &&
|
|
||||||
rsv1.FailedCount == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
//msgp:tuple replicationStats
|
|
||||||
type replicationStats struct {
|
|
||||||
PendingSize uint64
|
|
||||||
ReplicatedSize uint64
|
|
||||||
FailedSize uint64
|
|
||||||
FailedCount uint64
|
|
||||||
PendingCount uint64
|
|
||||||
MissedThresholdSize uint64
|
|
||||||
AfterThresholdSize uint64
|
|
||||||
MissedThresholdCount uint64
|
|
||||||
AfterThresholdCount uint64
|
|
||||||
ReplicatedCount uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs replicationStats) Empty() bool {
|
|
||||||
return rs.ReplicatedSize == 0 &&
|
|
||||||
rs.FailedSize == 0 &&
|
|
||||||
rs.FailedCount == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type replicationAllStats struct {
|
|
||||||
Targets map[string]replicationStats `msg:"t,omitempty"`
|
|
||||||
ReplicaSize uint64 `msg:"r,omitempty"`
|
|
||||||
ReplicaCount uint64 `msg:"rc,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
//msgp:tuple replicationAllStatsV1
|
|
||||||
type replicationAllStatsV1 struct {
|
|
||||||
Targets map[string]replicationStats
|
|
||||||
ReplicaSize uint64 `msg:"ReplicaSize,omitempty"`
|
|
||||||
ReplicaCount uint64 `msg:"ReplicaCount,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty returns true if the replicationAllStats is empty (contains no entries).
|
|
||||||
func (r *replicationAllStats) empty() bool {
|
|
||||||
if r == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if r.ReplicaSize != 0 || r.ReplicaCount != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, v := range r.Targets {
|
|
||||||
if !v.Empty() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// clone creates a deep-copy clone.
|
|
||||||
func (r *replicationAllStats) clone() *replicationAllStats {
|
|
||||||
if r == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shallow copy
|
|
||||||
dst := *r
|
|
||||||
|
|
||||||
// Copy individual targets.
|
|
||||||
dst.Targets = make(map[string]replicationStats, len(r.Targets))
|
|
||||||
for k, v := range r.Targets {
|
|
||||||
dst.Targets[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dst
|
|
||||||
}
|
|
||||||
|
|
||||||
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
//msgp:encode ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
||||||
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
//msgp:marshal ignore dataUsageEntryV2 dataUsageEntryV3 dataUsageEntryV4 dataUsageEntryV5 dataUsageEntryV6 dataUsageEntryV7
|
||||||
|
|
||||||
@@ -237,62 +150,54 @@ type dataUsageEntryV2 struct {
|
|||||||
//msgp:tuple dataUsageEntryV3
|
//msgp:tuple dataUsageEntryV3
|
||||||
type dataUsageEntryV3 struct {
|
type dataUsageEntryV3 struct {
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64
|
Size int64
|
||||||
ReplicatedSize uint64
|
Objects uint64
|
||||||
ReplicationPendingSize uint64
|
ObjSizes sizeHistogram
|
||||||
ReplicationFailedSize uint64
|
Children dataUsageHashMap
|
||||||
ReplicaSize uint64
|
|
||||||
Objects uint64
|
|
||||||
ObjSizes sizeHistogram
|
|
||||||
Children dataUsageHashMap
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//msgp:tuple dataUsageEntryV4
|
//msgp:tuple dataUsageEntryV4
|
||||||
type dataUsageEntryV4 struct {
|
type dataUsageEntryV4 struct {
|
||||||
Children dataUsageHashMap
|
Children dataUsageHashMap
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64
|
Size int64
|
||||||
Objects uint64
|
Objects uint64
|
||||||
ObjSizes sizeHistogram
|
ObjSizes sizeHistogram
|
||||||
ReplicationStats replicationStatsV1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//msgp:tuple dataUsageEntryV5
|
//msgp:tuple dataUsageEntryV5
|
||||||
type dataUsageEntryV5 struct {
|
type dataUsageEntryV5 struct {
|
||||||
Children dataUsageHashMap
|
Children dataUsageHashMap
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64
|
Size int64
|
||||||
Objects uint64
|
Objects uint64
|
||||||
Versions uint64 // Versions that are not delete markers.
|
Versions uint64 // Versions that are not delete markers.
|
||||||
ObjSizes sizeHistogram
|
ObjSizes sizeHistogram
|
||||||
ReplicationStats *replicationStatsV1
|
Compacted bool
|
||||||
Compacted bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//msgp:tuple dataUsageEntryV6
|
//msgp:tuple dataUsageEntryV6
|
||||||
type dataUsageEntryV6 struct {
|
type dataUsageEntryV6 struct {
|
||||||
Children dataUsageHashMap
|
Children dataUsageHashMap
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64
|
Size int64
|
||||||
Objects uint64
|
Objects uint64
|
||||||
Versions uint64 // Versions that are not delete markers.
|
Versions uint64 // Versions that are not delete markers.
|
||||||
ObjSizes sizeHistogram
|
ObjSizes sizeHistogram
|
||||||
ReplicationStats *replicationAllStatsV1
|
Compacted bool
|
||||||
Compacted bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type dataUsageEntryV7 struct {
|
type dataUsageEntryV7 struct {
|
||||||
Children dataUsageHashMap `msg:"ch"`
|
Children dataUsageHashMap `msg:"ch"`
|
||||||
// These fields do no include any children.
|
// These fields do no include any children.
|
||||||
Size int64 `msg:"sz"`
|
Size int64 `msg:"sz"`
|
||||||
Objects uint64 `msg:"os"`
|
Objects uint64 `msg:"os"`
|
||||||
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
Versions uint64 `msg:"vs"` // Versions that are not delete markers.
|
||||||
DeleteMarkers uint64 `msg:"dms"`
|
DeleteMarkers uint64 `msg:"dms"`
|
||||||
ObjSizes sizeHistogramV1 `msg:"szs"`
|
ObjSizes sizeHistogramV1 `msg:"szs"`
|
||||||
ObjVersions versionsHistogram `msg:"vh"`
|
ObjVersions versionsHistogram `msg:"vh"`
|
||||||
ReplicationStats *replicationAllStats `msg:"rs,omitempty"`
|
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
||||||
AllTierStats *allTierStats `msg:"ats,omitempty"`
|
Compacted bool `msg:"c"`
|
||||||
Compacted bool `msg:"c"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dataUsageCache contains a cache of data usage entries latest version.
|
// dataUsageCache contains a cache of data usage entries latest version.
|
||||||
@@ -373,29 +278,6 @@ func (e *dataUsageEntry) addSizes(summary sizeSummary) {
|
|||||||
e.ObjSizes.add(summary.totalSize)
|
e.ObjSizes.add(summary.totalSize)
|
||||||
e.ObjVersions.add(summary.versions)
|
e.ObjVersions.add(summary.versions)
|
||||||
|
|
||||||
if e.ReplicationStats == nil {
|
|
||||||
e.ReplicationStats = &replicationAllStats{
|
|
||||||
Targets: make(map[string]replicationStats),
|
|
||||||
}
|
|
||||||
} else if e.ReplicationStats.Targets == nil {
|
|
||||||
e.ReplicationStats.Targets = make(map[string]replicationStats)
|
|
||||||
}
|
|
||||||
e.ReplicationStats.ReplicaSize += uint64(summary.replicaSize)
|
|
||||||
e.ReplicationStats.ReplicaCount += uint64(summary.replicaCount)
|
|
||||||
|
|
||||||
for arn, st := range summary.replTargetStats {
|
|
||||||
tgtStat, ok := e.ReplicationStats.Targets[arn]
|
|
||||||
if !ok {
|
|
||||||
tgtStat = replicationStats{}
|
|
||||||
}
|
|
||||||
tgtStat.PendingSize += uint64(st.pendingSize)
|
|
||||||
tgtStat.FailedSize += uint64(st.failedSize)
|
|
||||||
tgtStat.ReplicatedSize += uint64(st.replicatedSize)
|
|
||||||
tgtStat.ReplicatedCount += uint64(st.replicatedCount)
|
|
||||||
tgtStat.FailedCount += st.failedCount
|
|
||||||
tgtStat.PendingCount += st.pendingCount
|
|
||||||
e.ReplicationStats.Targets[arn] = tgtStat
|
|
||||||
}
|
|
||||||
if len(summary.tiers) != 0 {
|
if len(summary.tiers) != 0 {
|
||||||
if e.AllTierStats == nil {
|
if e.AllTierStats == nil {
|
||||||
e.AllTierStats = newAllTierStats()
|
e.AllTierStats = newAllTierStats()
|
||||||
@@ -410,26 +292,6 @@ func (e *dataUsageEntry) merge(other dataUsageEntry) {
|
|||||||
e.Versions += other.Versions
|
e.Versions += other.Versions
|
||||||
e.DeleteMarkers += other.DeleteMarkers
|
e.DeleteMarkers += other.DeleteMarkers
|
||||||
e.Size += other.Size
|
e.Size += other.Size
|
||||||
if other.ReplicationStats != nil {
|
|
||||||
if e.ReplicationStats == nil {
|
|
||||||
e.ReplicationStats = &replicationAllStats{Targets: make(map[string]replicationStats)}
|
|
||||||
} else if e.ReplicationStats.Targets == nil {
|
|
||||||
e.ReplicationStats.Targets = make(map[string]replicationStats)
|
|
||||||
}
|
|
||||||
e.ReplicationStats.ReplicaSize += other.ReplicationStats.ReplicaSize
|
|
||||||
e.ReplicationStats.ReplicaCount += other.ReplicationStats.ReplicaCount
|
|
||||||
for arn, stat := range other.ReplicationStats.Targets {
|
|
||||||
st := e.ReplicationStats.Targets[arn]
|
|
||||||
e.ReplicationStats.Targets[arn] = replicationStats{
|
|
||||||
PendingSize: stat.PendingSize + st.PendingSize,
|
|
||||||
FailedSize: stat.FailedSize + st.FailedSize,
|
|
||||||
ReplicatedSize: stat.ReplicatedSize + st.ReplicatedSize,
|
|
||||||
PendingCount: stat.PendingCount + st.PendingCount,
|
|
||||||
FailedCount: stat.FailedCount + st.FailedCount,
|
|
||||||
ReplicatedCount: stat.ReplicatedCount + st.ReplicatedCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, v := range other.ObjSizes[:] {
|
for i, v := range other.ObjSizes[:] {
|
||||||
e.ObjSizes[i] += v
|
e.ObjSizes[i] += v
|
||||||
@@ -490,10 +352,7 @@ func (e dataUsageEntry) clone() dataUsageEntry {
|
|||||||
}
|
}
|
||||||
e.Children = ch
|
e.Children = ch
|
||||||
}
|
}
|
||||||
if e.ReplicationStats != nil {
|
|
||||||
// Clone ReplicationStats
|
|
||||||
e.ReplicationStats = e.ReplicationStats.clone()
|
|
||||||
}
|
|
||||||
if e.AllTierStats != nil {
|
if e.AllTierStats != nil {
|
||||||
e.AllTierStats = e.AllTierStats.clone()
|
e.AllTierStats = e.AllTierStats.clone()
|
||||||
}
|
}
|
||||||
@@ -931,22 +790,6 @@ func (d *dataUsageCache) bucketsUsageInfo(buckets []BucketInfo) map[string]Bucke
|
|||||||
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
ObjectSizesHistogram: flat.ObjSizes.toMap(),
|
||||||
ObjectVersionsHistogram: flat.ObjVersions.toMap(),
|
ObjectVersionsHistogram: flat.ObjVersions.toMap(),
|
||||||
}
|
}
|
||||||
if flat.ReplicationStats != nil {
|
|
||||||
bui.ReplicaSize = flat.ReplicationStats.ReplicaSize
|
|
||||||
bui.ReplicaCount = flat.ReplicationStats.ReplicaCount
|
|
||||||
|
|
||||||
bui.ReplicationInfo = make(map[string]BucketTargetUsageInfo, len(flat.ReplicationStats.Targets))
|
|
||||||
for arn, stat := range flat.ReplicationStats.Targets {
|
|
||||||
bui.ReplicationInfo[arn] = BucketTargetUsageInfo{
|
|
||||||
ReplicationPendingSize: stat.PendingSize,
|
|
||||||
ReplicatedSize: stat.ReplicatedSize,
|
|
||||||
ReplicationFailedSize: stat.FailedSize,
|
|
||||||
ReplicationPendingCount: stat.PendingCount,
|
|
||||||
ReplicationFailedCount: stat.FailedCount,
|
|
||||||
ReplicatedCount: stat.ReplicatedCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dst[bucket.Name] = bui
|
dst[bucket.Name] = bui
|
||||||
}
|
}
|
||||||
return dst
|
return dst
|
||||||
@@ -959,9 +802,6 @@ func (d *dataUsageCache) sizeRecursive(path string) *dataUsageEntry {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
flat := d.flatten(*root)
|
flat := d.flatten(*root)
|
||||||
if flat.ReplicationStats.empty() {
|
|
||||||
flat.ReplicationStats = nil
|
|
||||||
}
|
|
||||||
return &flat
|
return &flat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1238,20 +1078,6 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
|||||||
ObjSizes: v.ObjSizes,
|
ObjSizes: v.ObjSizes,
|
||||||
Children: v.Children,
|
Children: v.Children,
|
||||||
}
|
}
|
||||||
if v.ReplicatedSize > 0 || v.ReplicaSize > 0 || v.ReplicationFailedSize > 0 || v.ReplicationPendingSize > 0 {
|
|
||||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
|
||||||
if cfg != nil && cfg.RoleArn != "" {
|
|
||||||
due.ReplicationStats = &replicationAllStats{
|
|
||||||
Targets: make(map[string]replicationStats),
|
|
||||||
}
|
|
||||||
due.ReplicationStats.ReplicaSize = v.ReplicaSize
|
|
||||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
|
||||||
ReplicatedSize: v.ReplicatedSize,
|
|
||||||
FailedSize: v.ReplicationFailedSize,
|
|
||||||
PendingSize: v.ReplicationPendingSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||||
|
|
||||||
d.Cache[k] = due
|
d.Cache[k] = due
|
||||||
@@ -1277,36 +1103,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
|||||||
ObjSizes: v.ObjSizes,
|
ObjSizes: v.ObjSizes,
|
||||||
Children: v.Children,
|
Children: v.Children,
|
||||||
}
|
}
|
||||||
empty := replicationStatsV1{}
|
|
||||||
|
|
||||||
if v.ReplicationStats != empty {
|
|
||||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
|
||||||
if cfg != nil && cfg.RoleArn != "" {
|
|
||||||
due.ReplicationStats = &replicationAllStats{
|
|
||||||
Targets: make(map[string]replicationStats),
|
|
||||||
}
|
|
||||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
|
||||||
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
|
|
||||||
FailedSize: v.ReplicationStats.FailedSize,
|
|
||||||
FailedCount: v.ReplicationStats.FailedCount,
|
|
||||||
PendingSize: v.ReplicationStats.PendingSize,
|
|
||||||
PendingCount: v.ReplicationStats.PendingCount,
|
|
||||||
}
|
|
||||||
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||||
|
|
||||||
d.Cache[k] = due
|
d.Cache[k] = due
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate compacted value and remove unneeded replica stats.
|
|
||||||
for k, e := range d.Cache {
|
|
||||||
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
|
|
||||||
e.ReplicationStats = nil
|
|
||||||
}
|
|
||||||
d.Cache[k] = e
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
case dataUsageCacheVerV5:
|
case dataUsageCacheVerV5:
|
||||||
// Zstd compressed.
|
// Zstd compressed.
|
||||||
@@ -1328,36 +1128,10 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
|||||||
ObjSizes: v.ObjSizes,
|
ObjSizes: v.ObjSizes,
|
||||||
Children: v.Children,
|
Children: v.Children,
|
||||||
}
|
}
|
||||||
if v.ReplicationStats != nil && !v.ReplicationStats.Empty() {
|
|
||||||
cfg, _ := getReplicationConfig(GlobalContext, d.Info.Name)
|
|
||||||
if cfg != nil && cfg.RoleArn != "" {
|
|
||||||
due.ReplicationStats = &replicationAllStats{
|
|
||||||
Targets: make(map[string]replicationStats),
|
|
||||||
}
|
|
||||||
d.Info.replication = replicationConfig{Config: cfg}
|
|
||||||
|
|
||||||
due.ReplicationStats.Targets[cfg.RoleArn] = replicationStats{
|
|
||||||
ReplicatedSize: v.ReplicationStats.ReplicatedSize,
|
|
||||||
FailedSize: v.ReplicationStats.FailedSize,
|
|
||||||
FailedCount: v.ReplicationStats.FailedCount,
|
|
||||||
PendingSize: v.ReplicationStats.PendingSize,
|
|
||||||
PendingCount: v.ReplicationStats.PendingCount,
|
|
||||||
}
|
|
||||||
due.ReplicationStats.ReplicaSize = v.ReplicationStats.ReplicaSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
due.Compacted = len(due.Children) == 0 && k != d.Info.Name
|
||||||
|
|
||||||
d.Cache[k] = due
|
d.Cache[k] = due
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate compacted value and remove unneeded replica stats.
|
|
||||||
for k, e := range d.Cache {
|
|
||||||
if e.ReplicationStats != nil && len(e.ReplicationStats.Targets) == 0 {
|
|
||||||
e.ReplicationStats = nil
|
|
||||||
}
|
|
||||||
d.Cache[k] = e
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
case dataUsageCacheVerV6:
|
case dataUsageCacheVerV6:
|
||||||
// Zstd compressed.
|
// Zstd compressed.
|
||||||
@@ -1373,22 +1147,13 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
|||||||
d.Info = dold.Info
|
d.Info = dold.Info
|
||||||
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
|
d.Cache = make(map[string]dataUsageEntry, len(dold.Cache))
|
||||||
for k, v := range dold.Cache {
|
for k, v := range dold.Cache {
|
||||||
var replicationStats *replicationAllStats
|
|
||||||
if v.ReplicationStats != nil {
|
|
||||||
replicationStats = &replicationAllStats{
|
|
||||||
Targets: v.ReplicationStats.Targets,
|
|
||||||
ReplicaSize: v.ReplicationStats.ReplicaSize,
|
|
||||||
ReplicaCount: v.ReplicationStats.ReplicaCount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
due := dataUsageEntry{
|
due := dataUsageEntry{
|
||||||
Children: v.Children,
|
Children: v.Children,
|
||||||
Size: v.Size,
|
Size: v.Size,
|
||||||
Objects: v.Objects,
|
Objects: v.Objects,
|
||||||
Versions: v.Versions,
|
Versions: v.Versions,
|
||||||
ObjSizes: v.ObjSizes,
|
ObjSizes: v.ObjSizes,
|
||||||
ReplicationStats: replicationStats,
|
Compacted: v.Compacted,
|
||||||
Compacted: v.Compacted,
|
|
||||||
}
|
}
|
||||||
d.Cache[k] = due
|
d.Cache[k] = due
|
||||||
}
|
}
|
||||||
@@ -1410,13 +1175,12 @@ func (d *dataUsageCache) deserialize(r io.Reader) error {
|
|||||||
var szHist sizeHistogram
|
var szHist sizeHistogram
|
||||||
szHist.mergeV1(v.ObjSizes)
|
szHist.mergeV1(v.ObjSizes)
|
||||||
d.Cache[k] = dataUsageEntry{
|
d.Cache[k] = dataUsageEntry{
|
||||||
Children: v.Children,
|
Children: v.Children,
|
||||||
Size: v.Size,
|
Size: v.Size,
|
||||||
Objects: v.Objects,
|
Objects: v.Objects,
|
||||||
Versions: v.Versions,
|
Versions: v.Versions,
|
||||||
ObjSizes: szHist,
|
ObjSizes: szHist,
|
||||||
ReplicationStats: v.ReplicationStats,
|
Compacted: v.Compacted,
|
||||||
Compacted: v.Compacted,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+615
-1330
File diff suppressed because it is too large
Load Diff
@@ -519,458 +519,6 @@ func BenchmarkDecodedataUsageEntry(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalUnmarshalreplicationAllStats(t *testing.T) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
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 BenchmarkMarshalMsgreplicationAllStats(b *testing.B) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
v.MarshalMsg(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAppendMsgreplicationAllStats(b *testing.B) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
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 BenchmarkUnmarshalreplicationAllStats(b *testing.B) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
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 TestEncodeDecodereplicationAllStats(t *testing.T) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
msgp.Encode(&buf, &v)
|
|
||||||
|
|
||||||
m := v.Msgsize()
|
|
||||||
if buf.Len() > m {
|
|
||||||
t.Log("WARNING: TestEncodeDecodereplicationAllStats Msgsize() is inaccurate")
|
|
||||||
}
|
|
||||||
|
|
||||||
vn := replicationAllStats{}
|
|
||||||
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 BenchmarkEncodereplicationAllStats(b *testing.B) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
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 BenchmarkDecodereplicationAllStats(b *testing.B) {
|
|
||||||
v := replicationAllStats{}
|
|
||||||
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 TestMarshalUnmarshalreplicationAllStatsV1(t *testing.T) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
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 BenchmarkMarshalMsgreplicationAllStatsV1(b *testing.B) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
v.MarshalMsg(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAppendMsgreplicationAllStatsV1(b *testing.B) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
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 BenchmarkUnmarshalreplicationAllStatsV1(b *testing.B) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
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 TestEncodeDecodereplicationAllStatsV1(t *testing.T) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
msgp.Encode(&buf, &v)
|
|
||||||
|
|
||||||
m := v.Msgsize()
|
|
||||||
if buf.Len() > m {
|
|
||||||
t.Log("WARNING: TestEncodeDecodereplicationAllStatsV1 Msgsize() is inaccurate")
|
|
||||||
}
|
|
||||||
|
|
||||||
vn := replicationAllStatsV1{}
|
|
||||||
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 BenchmarkEncodereplicationAllStatsV1(b *testing.B) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
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 BenchmarkDecodereplicationAllStatsV1(b *testing.B) {
|
|
||||||
v := replicationAllStatsV1{}
|
|
||||||
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 TestMarshalUnmarshalreplicationStats(t *testing.T) {
|
|
||||||
v := replicationStats{}
|
|
||||||
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 BenchmarkMarshalMsgreplicationStats(b *testing.B) {
|
|
||||||
v := replicationStats{}
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
v.MarshalMsg(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAppendMsgreplicationStats(b *testing.B) {
|
|
||||||
v := replicationStats{}
|
|
||||||
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 BenchmarkUnmarshalreplicationStats(b *testing.B) {
|
|
||||||
v := replicationStats{}
|
|
||||||
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 TestEncodeDecodereplicationStats(t *testing.T) {
|
|
||||||
v := replicationStats{}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
msgp.Encode(&buf, &v)
|
|
||||||
|
|
||||||
m := v.Msgsize()
|
|
||||||
if buf.Len() > m {
|
|
||||||
t.Log("WARNING: TestEncodeDecodereplicationStats Msgsize() is inaccurate")
|
|
||||||
}
|
|
||||||
|
|
||||||
vn := replicationStats{}
|
|
||||||
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 BenchmarkEncodereplicationStats(b *testing.B) {
|
|
||||||
v := replicationStats{}
|
|
||||||
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 BenchmarkDecodereplicationStats(b *testing.B) {
|
|
||||||
v := replicationStats{}
|
|
||||||
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 TestMarshalUnmarshalreplicationStatsV1(t *testing.T) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
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 BenchmarkMarshalMsgreplicationStatsV1(b *testing.B) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
v.MarshalMsg(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkAppendMsgreplicationStatsV1(b *testing.B) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
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 BenchmarkUnmarshalreplicationStatsV1(b *testing.B) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
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 TestEncodeDecodereplicationStatsV1(t *testing.T) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
msgp.Encode(&buf, &v)
|
|
||||||
|
|
||||||
m := v.Msgsize()
|
|
||||||
if buf.Len() > m {
|
|
||||||
t.Log("WARNING: TestEncodeDecodereplicationStatsV1 Msgsize() is inaccurate")
|
|
||||||
}
|
|
||||||
|
|
||||||
vn := replicationStatsV1{}
|
|
||||||
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 BenchmarkEncodereplicationStatsV1(b *testing.B) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
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 BenchmarkDecodereplicationStatsV1(b *testing.B) {
|
|
||||||
v := replicationStatsV1{}
|
|
||||||
var buf bytes.Buffer
|
|
||||||
msgp.Encode(&buf, &v)
|
|
||||||
b.SetBytes(int64(buf.Len()))
|
|
||||||
rd := msgp.NewEndlessReader(buf.Bytes(), b)
|
|
||||||
dc := msgp.NewReader(rd)
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
err := v.DecodeMsg(dc)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
|
func TestMarshalUnmarshalsizeHistogram(t *testing.T) {
|
||||||
v := sizeHistogram{}
|
v := sizeHistogram{}
|
||||||
bts, err := v.MarshalMsg(nil)
|
bts, err := v.MarshalMsg(nil)
|
||||||
|
|||||||
@@ -587,17 +587,6 @@ func TestDataUsageCacheSerialize(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
e := want.find("abucket/dir2")
|
e := want.find("abucket/dir2")
|
||||||
e.ReplicationStats = &replicationAllStats{
|
|
||||||
Targets: map[string]replicationStats{
|
|
||||||
"arn": {
|
|
||||||
PendingSize: 1,
|
|
||||||
ReplicatedSize: 2,
|
|
||||||
FailedSize: 3,
|
|
||||||
FailedCount: 5,
|
|
||||||
PendingCount: 6,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
want.replace("abucket/dir2", "", *e)
|
want.replace("abucket/dir2", "", *e)
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = want.serializeTo(&buf)
|
err = want.serializeTo(&buf)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func TestCmpReaders(t *testing.T) {
|
|||||||
r1 := bytes.NewReader([]byte("abc"))
|
r1 := bytes.NewReader([]byte("abc"))
|
||||||
r2 := bytes.NewReader([]byte("abc"))
|
r2 := bytes.NewReader([]byte("abc"))
|
||||||
ok, msg := cmpReaders(r1, r2)
|
ok, msg := cmpReaders(r1, r2)
|
||||||
if !(ok && msg == "") {
|
if !ok || msg != "" {
|
||||||
t.Fatalf("unexpected")
|
t.Fatalf("unexpected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
|
|||||||
|
|
||||||
// We leak entries while we copy
|
// We leak entries while we copy
|
||||||
if entries == dynamicTimeoutLogSize {
|
if entries == dynamicTimeoutLogSize {
|
||||||
|
|
||||||
// Make copy on stack in order to call adjust()
|
// Make copy on stack in order to call adjust()
|
||||||
logCopy := [dynamicTimeoutLogSize]time.Duration{}
|
logCopy := [dynamicTimeoutLogSize]time.Duration{}
|
||||||
copy(logCopy[:], dt.log[:])
|
copy(logCopy[:], dt.log[:])
|
||||||
@@ -117,12 +116,12 @@ func (dt *dynamicTimeout) logEntry(duration time.Duration) {
|
|||||||
// adjust changes the value of the dynamic timeout based on the
|
// adjust changes the value of the dynamic timeout based on the
|
||||||
// previous results
|
// previous results
|
||||||
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
||||||
failures, max := 0, time.Duration(0)
|
failures, maxDur := 0, time.Duration(0)
|
||||||
for _, dur := range entries[:] {
|
for _, dur := range entries[:] {
|
||||||
if dur == maxDuration {
|
if dur == maxDuration {
|
||||||
failures++
|
failures++
|
||||||
} else if dur > max {
|
} else if dur > maxDur {
|
||||||
max = dur
|
maxDur = dur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,12 +143,12 @@ func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
|
|||||||
} else if failPct < dynamicTimeoutDecreaseThresholdPct {
|
} else if failPct < dynamicTimeoutDecreaseThresholdPct {
|
||||||
// We are hitting the timeout relatively few times,
|
// We are hitting the timeout relatively few times,
|
||||||
// so decrease the timeout towards 25 % of maximum time spent.
|
// so decrease the timeout towards 25 % of maximum time spent.
|
||||||
max = max * 125 / 100
|
maxDur = maxDur * 125 / 100
|
||||||
|
|
||||||
timeout := atomic.LoadInt64(&dt.timeout)
|
timeout := atomic.LoadInt64(&dt.timeout)
|
||||||
if max < time.Duration(timeout) {
|
if maxDur < time.Duration(timeout) {
|
||||||
// Move 50% toward the max.
|
// Move 50% toward the max.
|
||||||
timeout = (int64(max) + timeout) / 2
|
timeout = (int64(maxDur) + timeout) / 2
|
||||||
}
|
}
|
||||||
if timeout < dt.minimum {
|
if timeout < dt.minimum {
|
||||||
timeout = dt.minimum
|
timeout = dt.minimum
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ func testDynamicTimeoutAdjust(t *testing.T, timeout *dynamicTimeout, f func() fl
|
|||||||
const successTimeout = 20 * time.Second
|
const successTimeout = 20 * time.Second
|
||||||
|
|
||||||
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
for i := 0; i < dynamicTimeoutLogSize; i++ {
|
||||||
|
|
||||||
rnd := f()
|
rnd := f()
|
||||||
duration := time.Duration(float64(successTimeout) * rnd)
|
duration := time.Duration(float64(successTimeout) * rnd)
|
||||||
|
|
||||||
|
|||||||
+11
-9
@@ -109,7 +109,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
|||||||
// be AWS S3 compliant.
|
// be AWS S3 compliant.
|
||||||
//
|
//
|
||||||
// DecryptETags uses a KMS bulk decryption API, if available, which
|
// DecryptETags uses a KMS bulk decryption API, if available, which
|
||||||
// is more efficient than decrypting ETags sequentually.
|
// is more efficient than decrypting ETags sequentially.
|
||||||
func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
||||||
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
||||||
var (
|
var (
|
||||||
@@ -347,8 +347,8 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
|||||||
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
return errInvalidSSEParameters // AWS returns special error for equal but invalid keys.
|
||||||
}
|
}
|
||||||
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
|
return crypto.ErrInvalidCustomerKey // To provide strict AWS S3 compatibility we return: access denied.
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
|
if subtle.ConstantTimeCompare(oldKey, newKey) == 1 && sealedKey.Algorithm == crypto.SealAlgorithm {
|
||||||
return nil // don't rotate on equal keys if seal algorithm is latest
|
return nil // don't rotate on equal keys if seal algorithm is latest
|
||||||
}
|
}
|
||||||
@@ -1015,7 +1015,7 @@ func DecryptObjectInfo(info *ObjectInfo, r *http.Request) (encrypted bool, err e
|
|||||||
|
|
||||||
if encrypted {
|
if encrypted {
|
||||||
if crypto.SSEC.IsEncrypted(info.UserDefined) {
|
if crypto.SSEC.IsEncrypted(info.UserDefined) {
|
||||||
if !(crypto.SSEC.IsRequested(headers) || crypto.SSECopy.IsRequested(headers)) {
|
if !crypto.SSEC.IsRequested(headers) && !crypto.SSECopy.IsRequested(headers) {
|
||||||
if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
|
if r.Header.Get(xhttp.MinIOSourceReplicationRequest) != "true" {
|
||||||
return encrypted, errEncryptedObject
|
return encrypted, errEncryptedObject
|
||||||
}
|
}
|
||||||
@@ -1099,7 +1099,9 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
|
|||||||
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
if _, encrypted := crypto.IsEncrypted(o.UserDefined); encrypted {
|
||||||
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
|
decrypted, err := o.metadataDecrypter(h)("object-checksum", data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
encLogIf(GlobalContext, err)
|
if !errors.Is(err, crypto.ErrSecretKeyMismatch) {
|
||||||
|
encLogIf(GlobalContext, err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data = decrypted
|
data = decrypted
|
||||||
@@ -1110,7 +1112,6 @@ func (o *ObjectInfo) decryptPartsChecksums(h http.Header) {
|
|||||||
o.Parts[i].Checksums = cs[i]
|
o.Parts[i].Checksums = cs[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// metadataEncryptFn provides an encryption function for metadata.
|
// metadataEncryptFn provides an encryption function for metadata.
|
||||||
@@ -1153,16 +1154,17 @@ func (o *ObjectInfo) metadataEncryptFn(headers http.Header) (objectMetaEncryptFn
|
|||||||
|
|
||||||
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
// decryptChecksums will attempt to decode checksums and return it/them if set.
|
||||||
// if part > 0, and we have the checksum for the part that will be returned.
|
// if part > 0, and we have the checksum for the part that will be returned.
|
||||||
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string {
|
// Returns whether the checksum (main part 0) is a multipart checksum.
|
||||||
|
func (o *ObjectInfo) decryptChecksums(part int, h http.Header) (cs map[string]string, isMP bool) {
|
||||||
data := o.Checksum
|
data := o.Checksum
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil, false
|
||||||
}
|
}
|
||||||
if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
|
if part > 0 && !crypto.SSEC.IsEncrypted(o.UserDefined) {
|
||||||
// already decrypted in ToObjectInfo for multipart objects
|
// already decrypted in ToObjectInfo for multipart objects
|
||||||
for _, pi := range o.Parts {
|
for _, pi := range o.Parts {
|
||||||
if pi.Number == part {
|
if pi.Number == part {
|
||||||
return pi.Checksums
|
return pi.Checksums, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1172,7 +1174,7 @@ func (o *ObjectInfo) decryptChecksums(part int, h http.Header) map[string]string
|
|||||||
if err != crypto.ErrSecretKeyMismatch {
|
if err != crypto.ErrSecretKeyMismatch {
|
||||||
encLogIf(GlobalContext, err)
|
encLogIf(GlobalContext, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil, part > 0
|
||||||
}
|
}
|
||||||
data = decrypted
|
data = decrypted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,7 +362,6 @@ func TestGetDecryptedRange(t *testing.T) {
|
|||||||
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
t.Errorf("Case %d: test failed: %d %d %d %d %d", i, o, l, skip, sn, ps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multipart object tests
|
// Multipart object tests
|
||||||
@@ -538,7 +537,6 @@ func TestGetDecryptedRange(t *testing.T) {
|
|||||||
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
i, o, l, skip, sn, ps, oRef, lRef, skipRef, snRef, psRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-14
@@ -26,6 +26,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,7 +39,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/mountinfo"
|
"github.com/minio/minio/internal/mountinfo"
|
||||||
"github.com/minio/pkg/v3/env"
|
"github.com/minio/pkg/v3/env"
|
||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EndpointType - enum for endpoint type.
|
// EndpointType - enum for endpoint type.
|
||||||
@@ -138,6 +138,17 @@ func (endpoint *Endpoint) SetDiskIndex(i int) {
|
|||||||
endpoint.DiskIdx = i
|
endpoint.DiskIdx = i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidURLEndpoint(u *url.URL) bool {
|
||||||
|
// URL style of endpoint.
|
||||||
|
// Valid URL style endpoint is
|
||||||
|
// - Scheme field must contain "http" or "https"
|
||||||
|
// - All field should be empty except Host and Path.
|
||||||
|
isURLOk := (u.Scheme == "http" || u.Scheme == "https") &&
|
||||||
|
u.User == nil && u.Opaque == "" && !u.ForceQuery &&
|
||||||
|
u.RawQuery == "" && u.Fragment == ""
|
||||||
|
return isURLOk
|
||||||
|
}
|
||||||
|
|
||||||
// NewEndpoint - returns new endpoint based on given arguments.
|
// NewEndpoint - returns new endpoint based on given arguments.
|
||||||
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
func NewEndpoint(arg string) (ep Endpoint, e error) {
|
||||||
// isEmptyPath - check whether given path is not empty.
|
// isEmptyPath - check whether given path is not empty.
|
||||||
@@ -157,8 +168,7 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
// Valid URL style endpoint is
|
// Valid URL style endpoint is
|
||||||
// - Scheme field must contain "http" or "https"
|
// - Scheme field must contain "http" or "https"
|
||||||
// - All field should be empty except Host and Path.
|
// - All field should be empty except Host and Path.
|
||||||
if !((u.Scheme == "http" || u.Scheme == "https") &&
|
if !isValidURLEndpoint(u) {
|
||||||
u.User == nil && u.Opaque == "" && !u.ForceQuery && u.RawQuery == "" && u.Fragment == "") {
|
|
||||||
return ep, fmt.Errorf("invalid URL endpoint format")
|
return ep, fmt.Errorf("invalid URL endpoint format")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +223,6 @@ func NewEndpoint(arg string) (ep Endpoint, e error) {
|
|||||||
u.Path = u.Path[1:]
|
u.Path = u.Path[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Only check if the arg is an ip address and ask for scheme since its absent.
|
// Only check if the arg is an ip address and ask for scheme since its absent.
|
||||||
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
// localhost, example.com, any FQDN cannot be disambiguated from a regular file path such as
|
||||||
@@ -605,11 +614,8 @@ func (endpoints Endpoints) UpdateIsLocal() error {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
keepAliveTicker := time.NewTicker(500 * time.Millisecond)
|
||||||
defer keepAliveTicker.Stop()
|
defer keepAliveTicker.Stop()
|
||||||
for {
|
for !foundLocal && (epsResolved != len(endpoints)) {
|
||||||
// Break if the local endpoint is found already Or all the endpoints are resolved.
|
// Break if the local endpoint is found already Or all the endpoints are resolved.
|
||||||
if foundLocal || (epsResolved == len(endpoints)) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry infinitely on Kubernetes and Docker swarm.
|
// Retry infinitely on Kubernetes and Docker swarm.
|
||||||
// This is needed as the remote hosts are sometime
|
// This is needed as the remote hosts are sometime
|
||||||
@@ -795,11 +801,8 @@ func (p PoolEndpointList) UpdateIsLocal() error {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
keepAliveTicker := time.NewTicker(1 * time.Second)
|
keepAliveTicker := time.NewTicker(1 * time.Second)
|
||||||
defer keepAliveTicker.Stop()
|
defer keepAliveTicker.Stop()
|
||||||
for {
|
for !foundLocal && (epsResolved != epCount) {
|
||||||
// Break if the local endpoint is found already Or all the endpoints are resolved.
|
// Break if the local endpoint is found already Or all the endpoints are resolved.
|
||||||
if foundLocal || (epsResolved == epCount) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry infinitely on Kubernetes and Docker swarm.
|
// Retry infinitely on Kubernetes and Docker swarm.
|
||||||
// This is needed as the remote hosts are sometime
|
// This is needed as the remote hosts are sometime
|
||||||
@@ -1194,7 +1197,7 @@ func GetProxyEndpointLocalIndex(proxyEps []ProxyEndpoint) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetProxyEndpoints - get all endpoints that can be used to proxy list request.
|
// GetProxyEndpoints - get all endpoints that can be used to proxy list request.
|
||||||
func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint {
|
func GetProxyEndpoints(endpointServerPools EndpointServerPools, transport http.RoundTripper) []ProxyEndpoint {
|
||||||
var proxyEps []ProxyEndpoint
|
var proxyEps []ProxyEndpoint
|
||||||
|
|
||||||
proxyEpSet := set.NewStringSet()
|
proxyEpSet := set.NewStringSet()
|
||||||
@@ -1213,7 +1216,7 @@ func GetProxyEndpoints(endpointServerPools EndpointServerPools) []ProxyEndpoint
|
|||||||
|
|
||||||
proxyEps = append(proxyEps, ProxyEndpoint{
|
proxyEps = append(proxyEps, ProxyEndpoint{
|
||||||
Endpoint: endpoint,
|
Endpoint: endpoint,
|
||||||
Transport: globalRemoteTargetTransport,
|
Transport: transport,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ func erasureSelfTest() {
|
|||||||
ok = false
|
ok = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -277,23 +277,21 @@ func partNeedsHealing(partErrs []int) bool {
|
|||||||
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
|
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess && i != checkPartUnknown }) > -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasPartErr(partErrs []int) bool {
|
func countPartNotSuccess(partErrs []int) (c int) {
|
||||||
return slices.IndexFunc(partErrs, func(i int) bool { return i != checkPartSuccess }) > -1
|
for _, pe := range partErrs {
|
||||||
|
if pe != checkPartSuccess {
|
||||||
|
c++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// disksWithAllParts - This function needs to be called with
|
// checkObjectWithAllParts sets partsMetadata and onlineDisks when xl.meta is inexistant/corrupted or outdated
|
||||||
// []StorageAPI returned by listOnlineDisks. Returns,
|
// it also checks if the status of each part (corrupted, missing, ok) in each drive
|
||||||
//
|
func checkObjectWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
||||||
// - disks which have all parts specified in the latest xl.meta.
|
errs []error, latestMeta FileInfo, filterByETag bool, bucket, object string,
|
||||||
//
|
|
||||||
// - slice of errors about the state of data files on disk - can have
|
|
||||||
// a not-found error or a hash-mismatch error.
|
|
||||||
func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetadata []FileInfo,
|
|
||||||
errs []error, latestMeta FileInfo, bucket, object string,
|
|
||||||
scanMode madmin.HealScanMode,
|
scanMode madmin.HealScanMode,
|
||||||
) (availableDisks []StorageAPI, dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
|
) (dataErrsByDisk map[int][]int, dataErrsByPart map[int][]int) {
|
||||||
availableDisks = make([]StorageAPI, len(onlineDisks))
|
|
||||||
|
|
||||||
dataErrsByDisk = make(map[int][]int, len(onlineDisks))
|
dataErrsByDisk = make(map[int][]int, len(onlineDisks))
|
||||||
for i := range onlineDisks {
|
for i := range onlineDisks {
|
||||||
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
|
dataErrsByDisk[i] = make([]int, len(latestMeta.Parts))
|
||||||
@@ -325,29 +323,32 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
erasureDistributionReliable := true
|
erasureDistributionReliable := inconsistent <= len(partsMetadata)/2
|
||||||
if inconsistent > len(partsMetadata)/2 {
|
|
||||||
// If there are too many inconsistent files, then we can't trust erasure.Distribution (most likely
|
|
||||||
// because of bugs found in CopyObject/PutObjectTags) https://github.com/minio/minio/pull/10772
|
|
||||||
erasureDistributionReliable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
metaErrs := make([]error, len(errs))
|
metaErrs := make([]error, len(errs))
|
||||||
|
|
||||||
for i, onlineDisk := range onlineDisks {
|
for i := range onlineDisks {
|
||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
metaErrs[i] = errs[i]
|
metaErrs[i] = errs[i]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if onlineDisk == OfflineDisk {
|
if onlineDisks[i] == OfflineDisk {
|
||||||
metaErrs[i] = errDiskNotFound
|
metaErrs[i] = errDiskNotFound
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := partsMetadata[i]
|
meta := partsMetadata[i]
|
||||||
if !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir {
|
corrupted := false
|
||||||
|
if filterByETag {
|
||||||
|
corrupted = meta.Metadata["etag"] != latestMeta.Metadata["etag"]
|
||||||
|
} else {
|
||||||
|
corrupted = !meta.ModTime.Equal(latestMeta.ModTime) || meta.DataDir != latestMeta.DataDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if corrupted {
|
||||||
metaErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
|
onlineDisks[i] = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,6 +356,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
if !meta.IsValid() {
|
if !meta.IsValid() {
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
metaErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
|
onlineDisks[i] = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +367,7 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
// might have the right erasure distribution.
|
// might have the right erasure distribution.
|
||||||
partsMetadata[i] = FileInfo{}
|
partsMetadata[i] = FileInfo{}
|
||||||
metaErrs[i] = errFileCorrupt
|
metaErrs[i] = errFileCorrupt
|
||||||
|
onlineDisks[i] = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,8 +388,8 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
if metaErrs[i] != nil {
|
if metaErrs[i] != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
meta := partsMetadata[i]
|
|
||||||
|
|
||||||
|
meta := partsMetadata[i]
|
||||||
if meta.Deleted || meta.IsRemote() {
|
if meta.Deleted || meta.IsRemote() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -408,7 +411,6 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
verifyResp *CheckPartsResp
|
verifyResp *CheckPartsResp
|
||||||
)
|
)
|
||||||
|
|
||||||
meta.DataDir = latestMeta.DataDir
|
|
||||||
switch scanMode {
|
switch scanMode {
|
||||||
case madmin.HealDeepScan:
|
case madmin.HealDeepScan:
|
||||||
// disk has a valid xl.meta but may not have all the
|
// disk has a valid xl.meta but may not have all the
|
||||||
@@ -434,16 +436,5 @@ func disksWithAllParts(ctx context.Context, onlineDisks []StorageAPI, partsMetad
|
|||||||
dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
|
dataErrsByDisk[disk][part] = dataErrsByPart[part][disk]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, onlineDisk := range onlineDisks {
|
|
||||||
if metaErrs[i] == nil && !hasPartErr(dataErrsByDisk[i]) {
|
|
||||||
// All parts verified, mark it as all data available.
|
|
||||||
availableDisks[i] = onlineDisk
|
|
||||||
} else {
|
|
||||||
// upon errors just make that disk's fileinfo invalid
|
|
||||||
partsMetadata[i] = FileInfo{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,7 +304,6 @@ func TestListOnlineDisks(t *testing.T) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||||
@@ -313,11 +312,11 @@ func TestListOnlineDisks(t *testing.T) {
|
|||||||
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
t.Fatalf("Expected modTime to be equal to %v but was found to be %v",
|
||||||
test.expectedTime, modTime)
|
test.expectedTime, modTime)
|
||||||
}
|
}
|
||||||
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
_, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
test.errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if test._tamperBackend != noTamper {
|
if test._tamperBackend != noTamper {
|
||||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
|
||||||
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
||||||
erasureDisks[tamperedIndex])
|
erasureDisks[tamperedIndex])
|
||||||
}
|
}
|
||||||
@@ -485,7 +484,6 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
|||||||
f.Close()
|
f.Close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
rQuorum := len(errs) - z.serverPools[0].sets[0].defaultParityCount
|
||||||
@@ -495,11 +493,11 @@ func TestListOnlineDisksSmallObjects(t *testing.T) {
|
|||||||
test.expectedTime, modTime)
|
test.expectedTime, modTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
availableDisks, _, _ := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
_, _ = checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
test.errs, fi, bucket, object, madmin.HealDeepScan)
|
test.errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if test._tamperBackend != noTamper {
|
if test._tamperBackend != noTamper {
|
||||||
if tamperedIndex != -1 && availableDisks[tamperedIndex] != nil {
|
if tamperedIndex != -1 && onlineDisks[tamperedIndex] != nil {
|
||||||
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
t.Fatalf("Drive (%v) with part.1 missing is not a drive with available data",
|
||||||
erasureDisks[tamperedIndex])
|
erasureDisks[tamperedIndex])
|
||||||
}
|
}
|
||||||
@@ -545,6 +543,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
|
|
||||||
// Test 1: Test that all disks are returned without any failures with
|
// Test 1: Test that all disks are returned without any failures with
|
||||||
// unmodified meta data
|
// unmodified meta data
|
||||||
|
erasureDisks = s.getDisks()
|
||||||
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
|
partsMetadata, errs := readAllFileInfo(ctx, erasureDisks, "", bucket, object, "", false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to read xl meta data %v", err)
|
t.Fatalf("Failed to read xl meta data %v", err)
|
||||||
@@ -557,14 +556,10 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
|
|
||||||
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
erasureDisks, _, _ = listOnlineDisks(erasureDisks, partsMetadata, errs, readQuorum)
|
||||||
|
|
||||||
filteredDisks, _, dataErrsPerDisk := disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
dataErrsPerDisk, _ := checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
for diskIndex, disk := range erasureDisks {
|
||||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
|
||||||
}
|
|
||||||
|
|
||||||
for diskIndex, disk := range filteredDisks {
|
|
||||||
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
|
t.Errorf("Unexpected error: %v", dataErrsPerDisk[diskIndex])
|
||||||
}
|
}
|
||||||
@@ -575,17 +570,15 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test 2: Not synchronized modtime
|
// Test 2: Not synchronized modtime
|
||||||
|
erasureDisks = s.getDisks()
|
||||||
partsMetadataBackup := partsMetadata[0]
|
partsMetadataBackup := partsMetadata[0]
|
||||||
partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour)
|
partsMetadata[0].ModTime = partsMetadata[0].ModTime.Add(-1 * time.Hour)
|
||||||
|
|
||||||
errs = make([]error, len(erasureDisks))
|
errs = make([]error, len(erasureDisks))
|
||||||
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
_, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
for diskIndex, disk := range erasureDisks {
|
||||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
|
||||||
}
|
|
||||||
for diskIndex, disk := range filteredDisks {
|
|
||||||
if diskIndex == 0 && disk != nil {
|
if diskIndex == 0 && disk != nil {
|
||||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||||
}
|
}
|
||||||
@@ -596,17 +589,15 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
partsMetadata[0] = partsMetadataBackup // Revert before going to the next test
|
partsMetadata[0] = partsMetadataBackup // Revert before going to the next test
|
||||||
|
|
||||||
// Test 3: Not synchronized DataDir
|
// Test 3: Not synchronized DataDir
|
||||||
|
erasureDisks = s.getDisks()
|
||||||
partsMetadataBackup = partsMetadata[1]
|
partsMetadataBackup = partsMetadata[1]
|
||||||
partsMetadata[1].DataDir = "foo-random"
|
partsMetadata[1].DataDir = "foo-random"
|
||||||
|
|
||||||
errs = make([]error, len(erasureDisks))
|
errs = make([]error, len(erasureDisks))
|
||||||
filteredDisks, _, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
_, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
for diskIndex, disk := range erasureDisks {
|
||||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
|
||||||
}
|
|
||||||
for diskIndex, disk := range filteredDisks {
|
|
||||||
if diskIndex == 1 && disk != nil {
|
if diskIndex == 1 && disk != nil {
|
||||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
||||||
}
|
}
|
||||||
@@ -617,6 +608,7 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
partsMetadata[1] = partsMetadataBackup // Revert before going to the next test
|
partsMetadata[1] = partsMetadataBackup // Revert before going to the next test
|
||||||
|
|
||||||
// Test 4: key = disk index, value = part name with hash mismatch
|
// Test 4: key = disk index, value = part name with hash mismatch
|
||||||
|
erasureDisks = s.getDisks()
|
||||||
diskFailures := make(map[int]string)
|
diskFailures := make(map[int]string)
|
||||||
diskFailures[0] = "part.1"
|
diskFailures[0] = "part.1"
|
||||||
diskFailures[3] = "part.1"
|
diskFailures[3] = "part.1"
|
||||||
@@ -637,29 +629,18 @@ func TestDisksWithAllParts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errs = make([]error, len(erasureDisks))
|
errs = make([]error, len(erasureDisks))
|
||||||
filteredDisks, dataErrsPerDisk, _ = disksWithAllParts(ctx, erasureDisks, partsMetadata,
|
dataErrsPerDisk, _ = checkObjectWithAllParts(ctx, erasureDisks, partsMetadata,
|
||||||
errs, fi, bucket, object, madmin.HealDeepScan)
|
errs, fi, false, bucket, object, madmin.HealDeepScan)
|
||||||
|
|
||||||
if len(filteredDisks) != len(erasureDisks) {
|
for diskIndex := range erasureDisks {
|
||||||
t.Errorf("Unexpected number of drives: %d", len(filteredDisks))
|
|
||||||
}
|
|
||||||
|
|
||||||
for diskIndex, disk := range filteredDisks {
|
|
||||||
if _, ok := diskFailures[diskIndex]; ok {
|
if _, ok := diskFailures[diskIndex]; ok {
|
||||||
if disk != nil {
|
|
||||||
t.Errorf("Drive not filtered as expected, drive: %d", diskIndex)
|
|
||||||
}
|
|
||||||
if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
if !partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
|
t.Errorf("Disk expected to be healed, driveIndex: %d", diskIndex)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if disk == nil {
|
|
||||||
t.Errorf("Drive erroneously filtered, driveIndex: %d", diskIndex)
|
|
||||||
}
|
|
||||||
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
if partNeedsHealing(dataErrsPerDisk[diskIndex]) {
|
||||||
t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
|
t.Errorf("Disk not expected to be healed, driveIndex: %d", diskIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-90
@@ -32,7 +32,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/grid"
|
"github.com/minio/minio/internal/grid"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/sync/errgroup"
|
"github.com/minio/pkg/v3/sync/errgroup"
|
||||||
"golang.org/x/exp/slices"
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
|
//go:generate stringer -type=healingMetric -trimprefix=healingMetric $GOFILE
|
||||||
@@ -45,7 +45,8 @@ const (
|
|||||||
healingMetricCheckAbandonedParts
|
healingMetricCheckAbandonedParts
|
||||||
)
|
)
|
||||||
|
|
||||||
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error {
|
// List a prefix or a single object versions and heal
|
||||||
|
func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string, recursive bool, scanMode madmin.HealScanMode, healEntry func(string, metaCacheEntry, madmin.HealScanMode) error) error {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -78,11 +79,14 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
|||||||
bucket: bucket,
|
bucket: bucket,
|
||||||
path: path,
|
path: path,
|
||||||
filterPrefix: filterPrefix,
|
filterPrefix: filterPrefix,
|
||||||
recursive: true,
|
recursive: recursive,
|
||||||
forwardTo: "",
|
forwardTo: "",
|
||||||
minDisks: 1,
|
minDisks: 1,
|
||||||
reportNotFound: false,
|
reportNotFound: false,
|
||||||
agreed: func(entry metaCacheEntry) {
|
agreed: func(entry metaCacheEntry) {
|
||||||
|
if !recursive && prefix != entry.name {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := healEntry(bucket, entry, scanMode); err != nil {
|
if err := healEntry(bucket, entry, scanMode); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,9 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
|||||||
// proceed to heal nonetheless.
|
// proceed to heal nonetheless.
|
||||||
entry, _ = entries.firstFound()
|
entry, _ = entries.firstFound()
|
||||||
}
|
}
|
||||||
|
if !recursive && prefix != entry.name {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err := healEntry(bucket, *entry, scanMode); err != nil {
|
if err := healEntry(bucket, *entry, scanMode); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return
|
return
|
||||||
@@ -112,9 +118,8 @@ func (er erasureObjects) listAndHeal(ctx context.Context, bucket, prefix string,
|
|||||||
|
|
||||||
// listAllBuckets lists all buckets from all disks. It also
|
// listAllBuckets lists all buckets from all disks. It also
|
||||||
// returns the occurrence of each buckets in all disks
|
// returns the occurrence of each buckets in all disks
|
||||||
func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets map[string]VolInfo, readQuorum int) error {
|
func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
|
||||||
g := errgroup.WithNErrs(len(storageDisks))
|
g := errgroup.WithNErrs(len(storageDisks))
|
||||||
var mu sync.Mutex
|
|
||||||
for index := range storageDisks {
|
for index := range storageDisks {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
@@ -126,6 +131,7 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, volInfo := range volsInfo {
|
for _, volInfo := range volsInfo {
|
||||||
// StorageAPI can send volume names which are
|
// StorageAPI can send volume names which are
|
||||||
// incompatible with buckets - these are
|
// incompatible with buckets - these are
|
||||||
@@ -133,53 +139,75 @@ func listAllBuckets(ctx context.Context, storageDisks []StorageAPI, healBuckets
|
|||||||
if isReservedOrInvalidBucket(volInfo.Name, false) {
|
if isReservedOrInvalidBucket(volInfo.Name, false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mu.Lock()
|
|
||||||
if _, ok := healBuckets[volInfo.Name]; !ok {
|
healBuckets.Compute(volInfo.Name, func(oldValue VolInfo, loaded bool) (newValue VolInfo, del bool) {
|
||||||
healBuckets[volInfo.Name] = volInfo
|
if loaded {
|
||||||
}
|
newValue = oldValue
|
||||||
mu.Unlock()
|
newValue.count = oldValue.count + 1
|
||||||
|
return newValue, false
|
||||||
|
}
|
||||||
|
return VolInfo{
|
||||||
|
Name: volInfo.Name,
|
||||||
|
Created: volInfo.Created,
|
||||||
|
count: 1,
|
||||||
|
}, false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}, index)
|
}, index)
|
||||||
}
|
}
|
||||||
return reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum)
|
|
||||||
|
if err := reduceReadQuorumErrs(ctx, g.Wait(), bucketMetadataOpIgnoredErrs, readQuorum); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
healBuckets.Range(func(volName string, volInfo VolInfo) bool {
|
||||||
|
if volInfo.count < readQuorum {
|
||||||
|
healBuckets.Delete(volName)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var errLegacyXLMeta = errors.New("legacy XL meta")
|
var (
|
||||||
|
errLegacyXLMeta = errors.New("legacy XL meta")
|
||||||
var errOutdatedXLMeta = errors.New("outdated XL meta")
|
errOutdatedXLMeta = errors.New("outdated XL meta")
|
||||||
|
errPartCorrupt = errors.New("part corrupt")
|
||||||
var errPartMissingOrCorrupt = errors.New("part missing or corrupt")
|
errPartMissing = errors.New("part missing")
|
||||||
|
)
|
||||||
|
|
||||||
// Only heal on disks where we are sure that healing is needed. We can expand
|
// Only heal on disks where we are sure that healing is needed. We can expand
|
||||||
// this list as and when we figure out more errors can be added to this list safely.
|
// this list as and when we figure out more errors can be added to this list safely.
|
||||||
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, error) {
|
func shouldHealObjectOnDisk(erErr error, partsErrs []int, meta FileInfo, latestMeta FileInfo) (bool, bool, error) {
|
||||||
if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
|
if errors.Is(erErr, errFileNotFound) || errors.Is(erErr, errFileVersionNotFound) || errors.Is(erErr, errFileCorrupt) {
|
||||||
return true, erErr
|
return true, true, erErr
|
||||||
}
|
}
|
||||||
if erErr == nil {
|
if erErr == nil {
|
||||||
if meta.XLV1 {
|
if meta.XLV1 {
|
||||||
// Legacy means heal always
|
// Legacy means heal always
|
||||||
// always check first.
|
// always check first.
|
||||||
return true, errLegacyXLMeta
|
return true, true, errLegacyXLMeta
|
||||||
}
|
}
|
||||||
if !latestMeta.Equals(meta) {
|
if !latestMeta.Equals(meta) {
|
||||||
return true, errOutdatedXLMeta
|
return true, true, errOutdatedXLMeta
|
||||||
}
|
}
|
||||||
if !meta.Deleted && !meta.IsRemote() {
|
if !meta.Deleted && !meta.IsRemote() {
|
||||||
// If xl.meta was read fine but there may be problem with the part.N files.
|
// If xl.meta was read fine but there may be problem with the part.N files.
|
||||||
for _, partErr := range partsErrs {
|
for _, partErr := range partsErrs {
|
||||||
if slices.Contains([]int{
|
if partErr == checkPartFileNotFound {
|
||||||
checkPartFileNotFound,
|
return true, false, errPartMissing
|
||||||
checkPartFileCorrupt,
|
}
|
||||||
}, partErr) {
|
if partErr == checkPartFileCorrupt {
|
||||||
return true, errPartMissingOrCorrupt
|
return true, false, errPartCorrupt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, false, nil
|
||||||
}
|
}
|
||||||
return false, erErr
|
return false, false, erErr
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -234,11 +262,12 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
|
|||||||
}
|
}
|
||||||
|
|
||||||
b, a := result.GetCorruptedCounts()
|
b, a := result.GetCorruptedCounts()
|
||||||
if b == a {
|
if b > 0 && b == a {
|
||||||
opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b)
|
opts.Error = fmt.Sprintf("unable to heal %d corrupted blocks on drives", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
b, a = result.GetMissingCounts()
|
b, a = result.GetMissingCounts()
|
||||||
if b == a {
|
if b > 0 && b == a {
|
||||||
opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b)
|
opts.Error = fmt.Sprintf("unable to heal %d missing blocks on drives", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +282,21 @@ func (er *erasureObjects) auditHealObject(ctx context.Context, bucket, object, v
|
|||||||
auditLogInternal(ctx, opts)
|
auditLogInternal(ctx, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func objectErrToDriveState(reason error) string {
|
||||||
|
switch {
|
||||||
|
case reason == nil:
|
||||||
|
return madmin.DriveStateOk
|
||||||
|
case IsErr(reason, errDiskNotFound):
|
||||||
|
return madmin.DriveStateOffline
|
||||||
|
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissing, errOutdatedXLMeta, errLegacyXLMeta):
|
||||||
|
return madmin.DriveStateMissing
|
||||||
|
case IsErr(reason, errFileCorrupt, errPartCorrupt):
|
||||||
|
return madmin.DriveStateCorrupt
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%s (%s)", madmin.DriveStateUnknown, reason.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Heals an object by re-writing corrupt/missing erasure blocks.
|
// Heals an object by re-writing corrupt/missing erasure blocks.
|
||||||
func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
|
func (er *erasureObjects) healObject(ctx context.Context, bucket string, object string, versionID string, opts madmin.HealOpts) (result madmin.HealResultItem, err error) {
|
||||||
dryRun := opts.DryRun
|
dryRun := opts.DryRun
|
||||||
@@ -327,26 +371,20 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
|
|
||||||
// List of disks having latest version of the object xl.meta
|
// List of disks having latest version of the object xl.meta
|
||||||
// (by modtime).
|
// (by modtime).
|
||||||
onlineDisks, modTime, etag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
onlineDisks, quorumModTime, quorumETag := listOnlineDisks(storageDisks, partsMetadata, errs, readQuorum)
|
||||||
|
|
||||||
// Latest FileInfo for reference. If a valid metadata is not
|
// Latest FileInfo for reference. If a valid metadata is not
|
||||||
// present, it is as good as object not found.
|
// present, it is as good as object not found.
|
||||||
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, modTime, etag, readQuorum)
|
latestMeta, err := pickValidFileInfo(ctx, partsMetadata, quorumModTime, quorumETag, readQuorum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// List of disks having all parts as per latest metadata.
|
// No modtime quorum
|
||||||
// NOTE: do not pass in latestDisks to diskWithAllParts since
|
filterDisksByETag := quorumETag != ""
|
||||||
// the diskWithAllParts needs to reach the drive to ensure
|
|
||||||
// validity of the metadata content, we should make sure that
|
dataErrsByDisk, dataErrsByPart := checkObjectWithAllParts(ctx, onlineDisks, partsMetadata,
|
||||||
// we pass in disks as is for it to be verified. Once verified
|
errs, latestMeta, filterDisksByETag, bucket, object, scanMode)
|
||||||
// the disksWithAllParts() returns the actual disks that can be
|
|
||||||
// used here for reconstruction. This is done to ensure that
|
|
||||||
// we do not skip drives that have inconsistent metadata to be
|
|
||||||
// skipped from purging when they are stale.
|
|
||||||
availableDisks, dataErrsByDisk, dataErrsByPart := disksWithAllParts(ctx, onlineDisks, partsMetadata,
|
|
||||||
errs, latestMeta, bucket, object, scanMode)
|
|
||||||
|
|
||||||
var erasure Erasure
|
var erasure Erasure
|
||||||
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||||
@@ -367,26 +405,18 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
// data state and a list of outdated disks on which data needs
|
// data state and a list of outdated disks on which data needs
|
||||||
// to be healed.
|
// to be healed.
|
||||||
outDatedDisks := make([]StorageAPI, len(storageDisks))
|
outDatedDisks := make([]StorageAPI, len(storageDisks))
|
||||||
disksToHealCount := 0
|
disksToHealCount, xlMetaToHealCount := 0, 0
|
||||||
for i := range availableDisks {
|
for i := range onlineDisks {
|
||||||
yes, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
|
yes, isMeta, reason := shouldHealObjectOnDisk(errs[i], dataErrsByDisk[i], partsMetadata[i], latestMeta)
|
||||||
if yes {
|
if yes {
|
||||||
outDatedDisks[i] = storageDisks[i]
|
outDatedDisks[i] = storageDisks[i]
|
||||||
disksToHealCount++
|
disksToHealCount++
|
||||||
|
if isMeta {
|
||||||
|
xlMetaToHealCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
driveState := ""
|
driveState := objectErrToDriveState(reason)
|
||||||
switch {
|
|
||||||
case reason == nil:
|
|
||||||
driveState = madmin.DriveStateOk
|
|
||||||
case IsErr(reason, errDiskNotFound):
|
|
||||||
driveState = madmin.DriveStateOffline
|
|
||||||
case IsErr(reason, errFileNotFound, errFileVersionNotFound, errVolumeNotFound, errPartMissingOrCorrupt, errOutdatedXLMeta, errLegacyXLMeta):
|
|
||||||
driveState = madmin.DriveStateMissing
|
|
||||||
default:
|
|
||||||
// all remaining cases imply corrupt data/metadata
|
|
||||||
driveState = madmin.DriveStateCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||||
UUID: "",
|
UUID: "",
|
||||||
@@ -400,16 +430,6 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAllNotFound(errs) {
|
|
||||||
// File is fully gone, fileInfo is empty.
|
|
||||||
err := errFileNotFound
|
|
||||||
if versionID != "" {
|
|
||||||
err = errFileVersionNotFound
|
|
||||||
}
|
|
||||||
return er.defaultHealResult(FileInfo{}, storageDisks, storageEndpoints, errs,
|
|
||||||
bucket, object, versionID), err
|
|
||||||
}
|
|
||||||
|
|
||||||
if disksToHealCount == 0 {
|
if disksToHealCount == 0 {
|
||||||
// Nothing to heal!
|
// Nothing to heal!
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -421,7 +441,24 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !latestMeta.XLV1 && !latestMeta.Deleted && disksToHealCount > latestMeta.Erasure.ParityBlocks {
|
cannotHeal := !latestMeta.XLV1 && !latestMeta.Deleted && xlMetaToHealCount > latestMeta.Erasure.ParityBlocks
|
||||||
|
if cannotHeal && quorumETag != "" {
|
||||||
|
// This is an object that is supposed to be removed by the dangling code
|
||||||
|
// but we noticed that ETag is the same for all objects, let's give it a shot
|
||||||
|
cannotHeal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !latestMeta.Deleted && !latestMeta.IsRemote() {
|
||||||
|
// check if there is a part that lost its quorum
|
||||||
|
for _, partErrs := range dataErrsByPart {
|
||||||
|
if countPartNotSuccess(partErrs) > latestMeta.Erasure.ParityBlocks {
|
||||||
|
cannotHeal = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cannotHeal {
|
||||||
// Allow for dangling deletes, on versions that have DataDir missing etc.
|
// Allow for dangling deletes, on versions that have DataDir missing etc.
|
||||||
// this would end up restoring the correct readable versions.
|
// this would end up restoring the correct readable versions.
|
||||||
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
|
m, err := er.deleteIfDangling(ctx, bucket, object, partsMetadata, errs, dataErrsByPart, ObjectOptions{
|
||||||
@@ -459,16 +496,15 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
tmpID := mustGetUUID()
|
tmpID := mustGetUUID()
|
||||||
migrateDataDir := mustGetUUID()
|
migrateDataDir := mustGetUUID()
|
||||||
|
|
||||||
// Reorder so that we have data disks first and parity disks next.
|
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(onlineDisks) {
|
||||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(availableDisks) {
|
err := fmt.Errorf("unexpected file distribution (%v) from online disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||||
err := fmt.Errorf("unexpected file distribution (%v) from available disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
latestMeta.Erasure.Distribution, onlineDisks, bucket, object, versionID)
|
||||||
latestMeta.Erasure.Distribution, availableDisks, bucket, object, versionID)
|
healingLogOnceIf(ctx, err, "heal-object-online-disks")
|
||||||
healingLogOnceIf(ctx, err, "heal-object-available-disks")
|
|
||||||
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
return er.defaultHealResult(latestMeta, storageDisks, storageEndpoints, errs,
|
||||||
bucket, object, versionID), err
|
bucket, object, versionID), err
|
||||||
}
|
}
|
||||||
|
|
||||||
latestDisks := shuffleDisks(availableDisks, latestMeta.Erasure.Distribution)
|
latestDisks := shuffleDisks(onlineDisks, latestMeta.Erasure.Distribution)
|
||||||
|
|
||||||
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
|
if !latestMeta.Deleted && len(latestMeta.Erasure.Distribution) != len(outDatedDisks) {
|
||||||
err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
err := fmt.Errorf("unexpected file distribution (%v) from outdated disks (%v), looks like backend disks have been manually modified refusing to heal %s/%s(%s)",
|
||||||
@@ -539,12 +575,15 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
if disk == OfflineDisk {
|
if disk == OfflineDisk {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
thisPartErrs := shuffleCheckParts(dataErrsByPart[partIndex], latestMeta.Erasure.Distribution)
|
||||||
|
if thisPartErrs[i] != checkPartSuccess {
|
||||||
|
continue
|
||||||
|
}
|
||||||
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
checksumInfo := copyPartsMetadata[i].Erasure.GetChecksumInfo(partNumber)
|
||||||
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
partPath := pathJoin(object, srcDataDir, fmt.Sprintf("part.%d", partNumber))
|
||||||
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
readers[i] = newBitrotReader(disk, copyPartsMetadata[i].Data, bucket, partPath, tillOffset, checksumAlgo,
|
||||||
checksumInfo.Hash, erasure.ShardSize())
|
checksumInfo.Hash, erasure.ShardSize())
|
||||||
prefer[i] = disk.Hostname() == ""
|
prefer[i] = disk.Hostname() == ""
|
||||||
|
|
||||||
}
|
}
|
||||||
writers := make([]io.Writer, len(outDatedDisks))
|
writers := make([]io.Writer, len(outDatedDisks))
|
||||||
for i, disk := range outDatedDisks {
|
for i, disk := range outDatedDisks {
|
||||||
@@ -569,7 +608,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
// later to the final location.
|
// later to the final location.
|
||||||
err = erasure.Heal(ctx, writers, readers, partSize, prefer)
|
err = erasure.Heal(ctx, writers, readers, partSize, prefer)
|
||||||
closeBitrotReaders(readers)
|
closeBitrotReaders(readers)
|
||||||
closeBitrotWriters(writers)
|
closeErrs := closeBitrotWriters(writers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -589,6 +628,13 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A non-nil stale disk which got error on Close()
|
||||||
|
if closeErrs[i] != nil {
|
||||||
|
outDatedDisks[i] = nil
|
||||||
|
disksToHealCount--
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
partsMetadata[i].DataDir = dstDataDir
|
partsMetadata[i].DataDir = dstDataDir
|
||||||
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
|
partsMetadata[i].AddObjectPart(partNumber, "", partSize, partActualSize, partModTime, partIdx, partChecksums)
|
||||||
if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
|
if len(inlineBuffers) > 0 && inlineBuffers[i] != nil {
|
||||||
@@ -603,9 +649,7 @@ func (er *erasureObjects) healObject(ctx context.Context, bucket string, object
|
|||||||
if disksToHealCount == 0 {
|
if disksToHealCount == 0 {
|
||||||
return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object)
|
return result, fmt.Errorf("all drives had write errors, unable to heal %s/%s", bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
|
defer er.deleteAll(context.Background(), minioMetaTmpBucket, tmpID)
|
||||||
@@ -806,13 +850,7 @@ func (er *erasureObjects) defaultHealResult(lfi FileInfo, storageDisks []Storage
|
|||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
driveState := madmin.DriveStateCorrupt
|
driveState := objectErrToDriveState(errs[index])
|
||||||
switch errs[index] {
|
|
||||||
case errFileNotFound, errVolumeNotFound:
|
|
||||||
driveState = madmin.DriveStateMissing
|
|
||||||
case nil:
|
|
||||||
driveState = madmin.DriveStateOk
|
|
||||||
}
|
|
||||||
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
result.Before.Drives = append(result.Before.Drives, madmin.HealDriveInfo{
|
||||||
UUID: "",
|
UUID: "",
|
||||||
Endpoint: storageEndpoints[index].String(),
|
Endpoint: storageEndpoints[index].String(),
|
||||||
@@ -905,12 +943,12 @@ func isObjectDirDangling(errs []error) (ok bool) {
|
|||||||
var foundNotEmpty int
|
var foundNotEmpty int
|
||||||
var otherFound int
|
var otherFound int
|
||||||
for _, readErr := range errs {
|
for _, readErr := range errs {
|
||||||
switch {
|
switch readErr {
|
||||||
case readErr == nil:
|
case nil:
|
||||||
found++
|
found++
|
||||||
case readErr == errFileNotFound || readErr == errVolumeNotFound:
|
case errFileNotFound, errVolumeNotFound:
|
||||||
notFound++
|
notFound++
|
||||||
case readErr == errVolumeNotEmpty:
|
case errVolumeNotEmpty:
|
||||||
foundNotEmpty++
|
foundNotEmpty++
|
||||||
default:
|
default:
|
||||||
otherFound++
|
otherFound++
|
||||||
|
|||||||
@@ -733,7 +733,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||||
return err
|
return err
|
||||||
@@ -780,7 +780,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
t.Fatalf("Expected versions 1, got %d", fileInfoPreHeal.NumVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||||
return err
|
return err
|
||||||
@@ -829,7 +829,7 @@ func TestHealingDanglingObject(t *testing.T) {
|
|||||||
t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions)
|
t.Fatalf("Expected versions 3, got %d", fileInfoPreHeal.NumVersions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Remove: true},
|
if err = objLayer.HealObjects(ctx, bucket, "", madmin.HealOpts{Recursive: true, Remove: true},
|
||||||
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
func(bucket, object, vid string, scanMode madmin.HealScanMode) error {
|
||||||
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
_, err := objLayer.HealObject(ctx, bucket, object, vid, madmin.HealOpts{ScanMode: scanMode, Remove: true})
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -52,15 +52,15 @@ func reduceCommonVersions(diskVersions [][]byte, writeQuorum int) (versions []by
|
|||||||
}
|
}
|
||||||
|
|
||||||
var commonVersions uint64
|
var commonVersions uint64
|
||||||
max := 0
|
maxCnt := 0
|
||||||
for versions, count := range diskVersionsCount {
|
for versions, count := range diskVersionsCount {
|
||||||
if max < count {
|
if maxCnt < count {
|
||||||
max = count
|
maxCnt = count
|
||||||
commonVersions = versions
|
commonVersions = versions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if max >= writeQuorum {
|
if maxCnt >= writeQuorum {
|
||||||
for _, versions := range diskVersions {
|
for _, versions := range diskVersions {
|
||||||
if binary.BigEndian.Uint64(versions) == commonVersions {
|
if binary.BigEndian.Uint64(versions) == commonVersions {
|
||||||
return versions
|
return versions
|
||||||
@@ -80,15 +80,15 @@ func reduceCommonDataDir(dataDirs []string, writeQuorum int) (dataDir string) {
|
|||||||
dataDirsCount[ddir]++
|
dataDirsCount[ddir]++
|
||||||
}
|
}
|
||||||
|
|
||||||
max := 0
|
maxCnt := 0
|
||||||
for ddir, count := range dataDirsCount {
|
for ddir, count := range dataDirsCount {
|
||||||
if max < count {
|
if maxCnt < count {
|
||||||
max = count
|
maxCnt = count
|
||||||
dataDir = ddir
|
dataDir = ddir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if max >= writeQuorum {
|
if maxCnt >= writeQuorum {
|
||||||
return dataDir
|
return dataDir
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,20 +115,20 @@ func reduceErrs(errs []error, ignoredErrs []error) (maxCount int, maxErr error)
|
|||||||
errorCounts[err]++
|
errorCounts[err]++
|
||||||
}
|
}
|
||||||
|
|
||||||
max := 0
|
maxCnt := 0
|
||||||
for err, count := range errorCounts {
|
for err, count := range errorCounts {
|
||||||
switch {
|
switch {
|
||||||
case max < count:
|
case maxCnt < count:
|
||||||
max = count
|
maxCnt = count
|
||||||
maxErr = err
|
maxErr = err
|
||||||
|
|
||||||
// Prefer `nil` over other error values with the same
|
// Prefer `nil` over other error values with the same
|
||||||
// number of occurrences.
|
// number of occurrences.
|
||||||
case max == count && err == nil:
|
case maxCnt == count && err == nil:
|
||||||
maxErr = err
|
maxErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return max, maxErr
|
return maxCnt, maxErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// reduceQuorumErrs behaves like reduceErrs by only for returning
|
// reduceQuorumErrs behaves like reduceErrs by only for returning
|
||||||
@@ -295,34 +295,34 @@ func shuffleDisksAndPartsMetadata(disks []StorageAPI, partsMetadata []FileInfo,
|
|||||||
return shuffledDisks, shuffledPartsMetadata
|
return shuffledDisks, shuffledPartsMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return shuffled partsMetadata depending on distribution.
|
func shuffleWithDist[T any](input []T, distribution []int) []T {
|
||||||
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) (shuffledPartsMetadata []FileInfo) {
|
|
||||||
if distribution == nil {
|
if distribution == nil {
|
||||||
return partsMetadata
|
return input
|
||||||
}
|
}
|
||||||
shuffledPartsMetadata = make([]FileInfo, len(partsMetadata))
|
shuffled := make([]T, len(input))
|
||||||
// Shuffle slice xl metadata for expected distribution.
|
for index := range input {
|
||||||
for index := range partsMetadata {
|
|
||||||
blockIndex := distribution[index]
|
blockIndex := distribution[index]
|
||||||
shuffledPartsMetadata[blockIndex-1] = partsMetadata[index]
|
shuffled[blockIndex-1] = input[index]
|
||||||
}
|
}
|
||||||
return shuffledPartsMetadata
|
return shuffled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return shuffled partsMetadata depending on distribution.
|
||||||
|
func shufflePartsMetadata(partsMetadata []FileInfo, distribution []int) []FileInfo {
|
||||||
|
return shuffleWithDist[FileInfo](partsMetadata, distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shuffleCheckParts - shuffle CheckParts slice depending on the
|
||||||
|
// erasure distribution.
|
||||||
|
func shuffleCheckParts(parts []int, distribution []int) []int {
|
||||||
|
return shuffleWithDist[int](parts, distribution)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shuffleDisks - shuffle input disks slice depending on the
|
// shuffleDisks - shuffle input disks slice depending on the
|
||||||
// erasure distribution. Return shuffled slice of disks with
|
// erasure distribution. Return shuffled slice of disks with
|
||||||
// their expected distribution.
|
// their expected distribution.
|
||||||
func shuffleDisks(disks []StorageAPI, distribution []int) (shuffledDisks []StorageAPI) {
|
func shuffleDisks(disks []StorageAPI, distribution []int) []StorageAPI {
|
||||||
if distribution == nil {
|
return shuffleWithDist[StorageAPI](disks, distribution)
|
||||||
return disks
|
|
||||||
}
|
|
||||||
shuffledDisks = make([]StorageAPI, len(disks))
|
|
||||||
// Shuffle disks for expected distribution.
|
|
||||||
for index := range disks {
|
|
||||||
blockIndex := distribution[index]
|
|
||||||
shuffledDisks[blockIndex-1] = disks[index]
|
|
||||||
}
|
|
||||||
return shuffledDisks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// evalDisks - returns a new slice of disks where nil is set if
|
// evalDisks - returns a new slice of disks where nil is set if
|
||||||
|
|||||||
@@ -514,8 +514,10 @@ func listObjectParities(partsMetadata []FileInfo, errs []error) (parities []int)
|
|||||||
if metadata.Deleted || metadata.Size == 0 {
|
if metadata.Deleted || metadata.Size == 0 {
|
||||||
parities[index] = totalShards / 2
|
parities[index] = totalShards / 2
|
||||||
} else if metadata.TransitionStatus == lifecycle.TransitionComplete {
|
} else if metadata.TransitionStatus == lifecycle.TransitionComplete {
|
||||||
// For tiered objects, read quorum is N/2+1 to ensure simple majority on xl.meta. It is not equal to EcM because the data integrity is entrusted with the warm tier.
|
// For tiered objects, read quorum is N/2+1 to ensure simple majority on xl.meta.
|
||||||
parities[index] = totalShards - (totalShards/2 + 1)
|
// It is not equal to EcM because the data integrity is entrusted with the warm tier.
|
||||||
|
// However, we never go below EcM, in case of a EcM=EcN setup.
|
||||||
|
parities[index] = max(totalShards-(totalShards/2+1), metadata.Erasure.ParityBlocks)
|
||||||
} else {
|
} else {
|
||||||
parities[index] = metadata.Erasure.ParityBlocks
|
parities[index] = metadata.Erasure.ParityBlocks
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-94
@@ -480,6 +480,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
|
|
||||||
if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() {
|
if opts.WantChecksum != nil && opts.WantChecksum.Type.IsSet() {
|
||||||
userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String()
|
userDefined[hash.MinIOMultipartChecksum] = opts.WantChecksum.Type.String()
|
||||||
|
userDefined[hash.MinIOMultipartChecksumType] = opts.WantChecksum.Type.ObjType()
|
||||||
}
|
}
|
||||||
|
|
||||||
modTime := opts.MTime
|
modTime := opts.MTime
|
||||||
@@ -508,6 +509,7 @@ func (er erasureObjects) newMultipartUpload(ctx context.Context, bucket string,
|
|||||||
return &NewMultipartUploadResult{
|
return &NewMultipartUploadResult{
|
||||||
UploadID: uploadID,
|
UploadID: uploadID,
|
||||||
ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum],
|
ChecksumAlgo: userDefined[hash.MinIOMultipartChecksum],
|
||||||
|
ChecksumType: userDefined[hash.MinIOMultipartChecksumType],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +528,14 @@ func (er erasureObjects) NewMultipartUpload(ctx context.Context, bucket, object
|
|||||||
|
|
||||||
// renamePart - renames multipart part to its relevant location under uploadID.
|
// renamePart - renames multipart part to its relevant location under uploadID.
|
||||||
func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, optsMeta []byte, writeQuorum int) ([]StorageAPI, error) {
|
func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, srcBucket, srcEntry, dstBucket, dstEntry string, optsMeta []byte, writeQuorum int) ([]StorageAPI, error) {
|
||||||
|
paths := []string{
|
||||||
|
dstEntry,
|
||||||
|
dstEntry + ".meta",
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup existing paths first across all drives.
|
||||||
|
er.cleanupMultipartPath(ctx, paths...)
|
||||||
|
|
||||||
g := errgroup.WithNErrs(len(disks))
|
g := errgroup.WithNErrs(len(disks))
|
||||||
|
|
||||||
// Rename file on all underlying storage disks.
|
// Rename file on all underlying storage disks.
|
||||||
@@ -542,11 +552,6 @@ func (er erasureObjects) renamePart(ctx context.Context, disks []StorageAPI, src
|
|||||||
// Wait for all renames to finish.
|
// Wait for all renames to finish.
|
||||||
errs := g.Wait()
|
errs := g.Wait()
|
||||||
|
|
||||||
paths := []string{
|
|
||||||
dstEntry,
|
|
||||||
dstEntry + ".meta",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
err := reduceWriteQuorumErrs(ctx, errs, objectOpIgnoredErrs, writeQuorum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
er.cleanupMultipartPath(ctx, paths...)
|
er.cleanupMultipartPath(ctx, paths...)
|
||||||
@@ -574,19 +579,9 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
return pi, toObjectErr(errInvalidArgument)
|
return pi, toObjectErr(errInvalidArgument)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read lock for upload id.
|
|
||||||
// Only held while reading the upload metadata.
|
|
||||||
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
|
||||||
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return PartInfo{}, err
|
|
||||||
}
|
|
||||||
rctx := rlkctx.Context()
|
|
||||||
defer uploadIDRLock.RUnlock(rlkctx)
|
|
||||||
|
|
||||||
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
uploadIDPath := er.getUploadIDDir(bucket, object, uploadID)
|
||||||
// Validates if upload ID exists.
|
// Validates if upload ID exists.
|
||||||
fi, _, err := er.checkUploadIDExists(rctx, bucket, object, uploadID, true)
|
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errVolumeNotFound) {
|
if errors.Is(err, errVolumeNotFound) {
|
||||||
return pi, toObjectErr(err, bucket)
|
return pi, toObjectErr(err, bucket)
|
||||||
@@ -633,17 +628,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
switch size := data.Size(); {
|
switch size := data.Size(); {
|
||||||
case size == 0:
|
case size == 0:
|
||||||
buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
|
buffer = make([]byte, 1) // Allocate at least a byte to reach EOF
|
||||||
case size == -1:
|
case size >= fi.Erasure.BlockSize || size == -1:
|
||||||
if size := data.ActualSize(); size > 0 && size < fi.Erasure.BlockSize {
|
if int64(globalBytePoolCap.Load().Width()) < fi.Erasure.BlockSize {
|
||||||
// Account for padding and forced compression overhead and encryption.
|
buffer = make([]byte, fi.Erasure.BlockSize, 2*fi.Erasure.BlockSize)
|
||||||
buffer = make([]byte, data.ActualSize()+256+32+32, data.ActualSize()*2+512)
|
|
||||||
} else {
|
} else {
|
||||||
buffer = globalBytePoolCap.Load().Get()
|
buffer = globalBytePoolCap.Load().Get()
|
||||||
defer globalBytePoolCap.Load().Put(buffer)
|
defer globalBytePoolCap.Load().Put(buffer)
|
||||||
}
|
}
|
||||||
case size >= fi.Erasure.BlockSize:
|
|
||||||
buffer = globalBytePoolCap.Load().Get()
|
|
||||||
defer globalBytePoolCap.Load().Put(buffer)
|
|
||||||
case size < fi.Erasure.BlockSize:
|
case size < fi.Erasure.BlockSize:
|
||||||
// No need to allocate fully fi.Erasure.BlockSize buffer if the incoming data is smaller.
|
// No need to allocate fully fi.Erasure.BlockSize buffer if the incoming data is smaller.
|
||||||
buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
|
buffer = make([]byte, size, 2*size+int64(fi.Erasure.ParityBlocks+fi.Erasure.DataBlocks-1))
|
||||||
@@ -677,10 +668,13 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
}
|
}
|
||||||
|
|
||||||
n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
n, err := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
||||||
closeBitrotWriters(writers)
|
closeErrs := closeBitrotWriters(writers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pi, toObjectErr(err, bucket, object)
|
return pi, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||||
|
return pi, toObjectErr(closeErr, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||||
// than specified in request header.
|
// than specified in request header.
|
||||||
@@ -741,10 +735,7 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
return pi, toObjectErr(err, minioMetaMultipartBucket, partPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write lock for this part ID, only hold it if we are planning to read from the
|
// Serialize concurrent part uploads.
|
||||||
// stream avoid any concurrent updates.
|
|
||||||
//
|
|
||||||
// Must be held throughout this call.
|
|
||||||
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
partIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID, strconv.Itoa(partID)))
|
||||||
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
|
plkctx, err := partIDLock.GetLock(ctx, globalOperationTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -754,6 +745,15 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
ctx = plkctx.Context()
|
ctx = plkctx.Context()
|
||||||
defer partIDLock.Unlock(plkctx)
|
defer partIDLock.Unlock(plkctx)
|
||||||
|
|
||||||
|
// Read lock for upload id, only held while reading the upload metadata.
|
||||||
|
uploadIDRLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
|
rlkctx, err := uploadIDRLock.GetRLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return PartInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = rlkctx.Context()
|
||||||
|
defer uploadIDRLock.RUnlock(rlkctx)
|
||||||
|
|
||||||
onlineDisks, err = er.renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, partFI, writeQuorum)
|
onlineDisks, err = er.renamePart(ctx, onlineDisks, minioMetaTmpBucket, tmpPartPath, minioMetaMultipartBucket, partPath, partFI, writeQuorum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errFileNotFound) {
|
if errors.Is(err, errFileNotFound) {
|
||||||
@@ -770,15 +770,16 @@ func (er erasureObjects) PutObjectPart(ctx context.Context, bucket, object, uplo
|
|||||||
|
|
||||||
// Return success.
|
// Return success.
|
||||||
return PartInfo{
|
return PartInfo{
|
||||||
PartNumber: partInfo.Number,
|
PartNumber: partInfo.Number,
|
||||||
ETag: partInfo.ETag,
|
ETag: partInfo.ETag,
|
||||||
LastModified: partInfo.ModTime,
|
LastModified: partInfo.ModTime,
|
||||||
Size: partInfo.Size,
|
Size: partInfo.Size,
|
||||||
ActualSize: partInfo.ActualSize,
|
ActualSize: partInfo.ActualSize,
|
||||||
ChecksumCRC32: partInfo.Checksums["CRC32"],
|
ChecksumCRC32: partInfo.Checksums["CRC32"],
|
||||||
ChecksumCRC32C: partInfo.Checksums["CRC32C"],
|
ChecksumCRC32C: partInfo.Checksums["CRC32C"],
|
||||||
ChecksumSHA1: partInfo.Checksums["SHA1"],
|
ChecksumSHA1: partInfo.Checksums["SHA1"],
|
||||||
ChecksumSHA256: partInfo.Checksums["SHA256"],
|
ChecksumSHA256: partInfo.Checksums["SHA256"],
|
||||||
|
ChecksumCRC64NVME: partInfo.Checksums["CRC64NVME"],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,14 +799,6 @@ func (er erasureObjects) GetMultipartInfo(ctx context.Context, bucket, object, u
|
|||||||
UploadID: uploadID,
|
UploadID: uploadID,
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
|
||||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return MultipartInfo{}, err
|
|
||||||
}
|
|
||||||
ctx = lkctx.Context()
|
|
||||||
defer uploadIDLock.RUnlock(lkctx)
|
|
||||||
|
|
||||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errVolumeNotFound) {
|
if errors.Is(err, errVolumeNotFound) {
|
||||||
@@ -885,14 +878,6 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
|||||||
auditObjectErasureSet(ctx, "ListObjectParts", object, &er)
|
auditObjectErasureSet(ctx, "ListObjectParts", object, &er)
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
|
||||||
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return ListPartsInfo{}, err
|
|
||||||
}
|
|
||||||
ctx = lkctx.Context()
|
|
||||||
defer uploadIDLock.RUnlock(lkctx)
|
|
||||||
|
|
||||||
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
fi, _, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, toObjectErr(err, bucket, object, uploadID)
|
return result, toObjectErr(err, bucket, object, uploadID)
|
||||||
@@ -916,6 +901,7 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
|||||||
result.PartNumberMarker = partNumberMarker
|
result.PartNumberMarker = partNumberMarker
|
||||||
result.UserDefined = cloneMSS(fi.Metadata)
|
result.UserDefined = cloneMSS(fi.Metadata)
|
||||||
result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum]
|
result.ChecksumAlgorithm = fi.Metadata[hash.MinIOMultipartChecksum]
|
||||||
|
result.ChecksumType = fi.Metadata[hash.MinIOMultipartChecksumType]
|
||||||
|
|
||||||
if maxParts == 0 {
|
if maxParts == 0 {
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -962,15 +948,16 @@ func (er erasureObjects) ListObjectParts(ctx context.Context, bucket, object, up
|
|||||||
count := maxParts
|
count := maxParts
|
||||||
for _, objPart := range objParts {
|
for _, objPart := range objParts {
|
||||||
result.Parts = append(result.Parts, PartInfo{
|
result.Parts = append(result.Parts, PartInfo{
|
||||||
PartNumber: objPart.Number,
|
PartNumber: objPart.Number,
|
||||||
LastModified: objPart.ModTime,
|
LastModified: objPart.ModTime,
|
||||||
ETag: objPart.ETag,
|
ETag: objPart.ETag,
|
||||||
Size: objPart.Size,
|
Size: objPart.Size,
|
||||||
ActualSize: objPart.ActualSize,
|
ActualSize: objPart.ActualSize,
|
||||||
ChecksumCRC32: objPart.Checksums["CRC32"],
|
ChecksumCRC32: objPart.Checksums["CRC32"],
|
||||||
ChecksumCRC32C: objPart.Checksums["CRC32C"],
|
ChecksumCRC32C: objPart.Checksums["CRC32C"],
|
||||||
ChecksumSHA1: objPart.Checksums["SHA1"],
|
ChecksumSHA1: objPart.Checksums["SHA1"],
|
||||||
ChecksumSHA256: objPart.Checksums["SHA256"],
|
ChecksumSHA256: objPart.Checksums["SHA256"],
|
||||||
|
ChecksumCRC64NVME: objPart.Checksums["CRC64NVME"],
|
||||||
})
|
})
|
||||||
count--
|
count--
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
@@ -1065,7 +1052,6 @@ func readParts(ctx context.Context, disks []StorageAPI, bucket string, partMetaP
|
|||||||
PartNumber: partNumbers[pidx],
|
PartNumber: partNumbers[pidx],
|
||||||
}.Error(),
|
}.Error(),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return partInfosInQuorum, nil
|
return partInfosInQuorum, nil
|
||||||
}
|
}
|
||||||
@@ -1115,16 +1101,6 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hold write locks to verify uploaded parts, also disallows any
|
|
||||||
// parallel PutObjectPart() requests.
|
|
||||||
uploadIDLock := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
|
||||||
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return oi, err
|
|
||||||
}
|
|
||||||
ctx = wlkctx.Context()
|
|
||||||
defer uploadIDLock.Unlock(wlkctx)
|
|
||||||
|
|
||||||
fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
|
fi, partsMetadata, err := er.checkUploadIDExists(ctx, bucket, object, uploadID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errVolumeNotFound) {
|
if errors.Is(err, errVolumeNotFound) {
|
||||||
@@ -1162,12 +1138,12 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
// Checksum type set when upload started.
|
// Checksum type set when upload started.
|
||||||
var checksumType hash.ChecksumType
|
var checksumType hash.ChecksumType
|
||||||
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
|
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
|
||||||
checksumType = hash.NewChecksumType(cs)
|
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
|
||||||
if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
|
if opts.WantChecksum != nil && !opts.WantChecksum.Type.Is(checksumType) {
|
||||||
return oi, InvalidArgument{
|
return oi, InvalidArgument{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: fi.Name,
|
Object: fi.Name,
|
||||||
Err: fmt.Errorf("checksum type mismatch"),
|
Err: fmt.Errorf("checksum type mismatch. got %q (%s) expected %q (%s)", checksumType.String(), checksumType.ObjType(), opts.WantChecksum.Type.String(), opts.WantChecksum.Type.ObjType()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1223,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
// Allocate parts similar to incoming slice.
|
// Allocate parts similar to incoming slice.
|
||||||
fi.Parts = make([]ObjectPartInfo, len(parts))
|
fi.Parts = make([]ObjectPartInfo, len(parts))
|
||||||
|
|
||||||
|
var checksum hash.Checksum
|
||||||
|
checksum.Type = checksumType
|
||||||
|
|
||||||
// Validate each part and then commit to disk.
|
// Validate each part and then commit to disk.
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
partIdx := objectPartIndex(currentFI.Parts, part.PartNumber)
|
partIdx := objectPartIndex(currentFI.Parts, part.PartNumber)
|
||||||
@@ -1280,10 +1259,11 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
wantCS := map[string]string{
|
wantCS := map[string]string{
|
||||||
hash.ChecksumCRC32.String(): part.ChecksumCRC32,
|
hash.ChecksumCRC32.String(): part.ChecksumCRC32,
|
||||||
hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
|
hash.ChecksumCRC32C.String(): part.ChecksumCRC32C,
|
||||||
hash.ChecksumSHA1.String(): part.ChecksumSHA1,
|
hash.ChecksumSHA1.String(): part.ChecksumSHA1,
|
||||||
hash.ChecksumSHA256.String(): part.ChecksumSHA256,
|
hash.ChecksumSHA256.String(): part.ChecksumSHA256,
|
||||||
|
hash.ChecksumCRC64NVME.String(): part.ChecksumCRC64NVME,
|
||||||
}
|
}
|
||||||
if wantCS[checksumType.String()] != crc {
|
if wantCS[checksumType.String()] != crc {
|
||||||
return oi, InvalidPart{
|
return oi, InvalidPart{
|
||||||
@@ -1298,6 +1278,15 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
PartNumber: part.PartNumber,
|
PartNumber: part.PartNumber,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if checksumType.FullObjectRequested() {
|
||||||
|
if err := checksum.AddPart(*cs, expPart.ActualSize); err != nil {
|
||||||
|
return oi, InvalidPart{
|
||||||
|
PartNumber: part.PartNumber,
|
||||||
|
ExpETag: "<nil>",
|
||||||
|
GotETag: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
checksumCombined = append(checksumCombined, cs.Raw...)
|
checksumCombined = append(checksumCombined, cs.Raw...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,9 +1317,19 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.WantChecksum != nil {
|
if opts.WantChecksum != nil {
|
||||||
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
if checksumType.FullObjectRequested() {
|
||||||
if err != nil {
|
if opts.WantChecksum.Encoded != checksum.Encoded {
|
||||||
return oi, err
|
err := hash.ChecksumMismatch{
|
||||||
|
Want: opts.WantChecksum.Encoded,
|
||||||
|
Got: checksum.Encoded,
|
||||||
|
}
|
||||||
|
return oi, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := opts.WantChecksum.Matches(checksumCombined, len(parts))
|
||||||
|
if err != nil {
|
||||||
|
return oi, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1344,14 +1343,18 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
|
|||||||
|
|
||||||
if checksumType.IsSet() {
|
if checksumType.IsSet() {
|
||||||
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
|
||||||
var cs *hash.Checksum
|
checksum.Type = checksumType
|
||||||
cs = hash.NewChecksumFromData(checksumType, checksumCombined)
|
if !checksumType.FullObjectRequested() {
|
||||||
fi.Checksum = cs.AppendTo(nil, checksumCombined)
|
checksum = *hash.NewChecksumFromData(checksumType, checksumCombined)
|
||||||
|
}
|
||||||
|
fi.Checksum = checksum.AppendTo(nil, checksumCombined)
|
||||||
if opts.EncryptFn != nil {
|
if opts.EncryptFn != nil {
|
||||||
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
fi.Checksum = opts.EncryptFn("object-checksum", fi.Checksum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete(fi.Metadata, hash.MinIOMultipartChecksum) // Not needed in final object.
|
// Remove superfluous internal headers.
|
||||||
|
delete(fi.Metadata, hash.MinIOMultipartChecksum)
|
||||||
|
delete(fi.Metadata, hash.MinIOMultipartChecksumType)
|
||||||
|
|
||||||
// Save the final object size and modtime.
|
// Save the final object size and modtime.
|
||||||
fi.Size = objectSize
|
fi.Size = objectSize
|
||||||
@@ -1491,14 +1494,6 @@ func (er erasureObjects) AbortMultipartUpload(ctx context.Context, bucket, objec
|
|||||||
auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er)
|
auditObjectErasureSet(ctx, "AbortMultipartUpload", object, &er)
|
||||||
}
|
}
|
||||||
|
|
||||||
lk := er.NewNSLock(bucket, pathJoin(object, uploadID))
|
|
||||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctx = lkctx.Context()
|
|
||||||
defer lk.Unlock(lkctx)
|
|
||||||
|
|
||||||
// Validates if upload ID exists.
|
// Validates if upload ID exists.
|
||||||
if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil {
|
if _, _, err = er.checkUploadIDExists(ctx, bucket, object, uploadID, false); err != nil {
|
||||||
if errors.Is(err, errVolumeNotFound) {
|
if errors.Is(err, errVolumeNotFound) {
|
||||||
|
|||||||
+77
-39
@@ -27,6 +27,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -102,7 +104,7 @@ func (er erasureObjects) CopyObject(ctx context.Context, srcBucket, srcObject, d
|
|||||||
|
|
||||||
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, writeQuorum, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(srcBucket, minioMetaBucket) {
|
if shouldCheckForDangling(err, errs, srcBucket) {
|
||||||
_, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts)
|
_, derr := er.deleteIfDangling(context.Background(), srcBucket, srcObject, metaArr, errs, nil, srcOpts)
|
||||||
if derr == nil {
|
if derr == nil {
|
||||||
if srcOpts.VersionID != "" {
|
if srcOpts.VersionID != "" {
|
||||||
@@ -690,13 +692,9 @@ func shouldCheckForDangling(err error, errs []error, bucket string) bool {
|
|||||||
// Check if we have a read quorum issue
|
// Check if we have a read quorum issue
|
||||||
case errors.Is(err, errErasureReadQuorum):
|
case errors.Is(err, errErasureReadQuorum):
|
||||||
return true
|
return true
|
||||||
// Check if the object is inexistent in most disks but not all of them
|
// Check if the object is non-existent on most disks but not all of them
|
||||||
case errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound):
|
case (errors.Is(err, errFileNotFound) || errors.Is(err, errFileVersionNotFound)) && (countErrs(errs, nil) > 0):
|
||||||
for i := range errs {
|
return true
|
||||||
if errs[i] == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -815,8 +813,6 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
|||||||
PoolIndex: er.poolIndex,
|
PoolIndex: er.poolIndex,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
validResp := 0
|
validResp := 0
|
||||||
@@ -921,6 +917,16 @@ func (er erasureObjects) getObjectFileInfo(ctx context.Context, bucket, object s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// when we have insufficient read quorum and inconsistent metadata return
|
||||||
|
// file not found, since we can't possibly have a way to recover this object
|
||||||
|
// anyway.
|
||||||
|
if v, ok := err.(InsufficientReadQuorum); ok && v.Type == RQInconsistentMeta {
|
||||||
|
if opts.VersionID != "" {
|
||||||
|
err = errFileVersionNotFound
|
||||||
|
} else {
|
||||||
|
err = errFileNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
return fi, nil, nil, toObjectErr(err, bucket, object)
|
return fi, nil, nil, toObjectErr(err, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1170,11 +1176,15 @@ func (er erasureObjects) putMetacacheObject(ctx context.Context, key string, r *
|
|||||||
}
|
}
|
||||||
|
|
||||||
n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
|
n, erasureErr := erasure.Encode(ctx, data, writers, buffer, writeQuorum)
|
||||||
closeBitrotWriters(writers)
|
closeErrs := closeBitrotWriters(writers)
|
||||||
if erasureErr != nil {
|
if erasureErr != nil {
|
||||||
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key)
|
return ObjectInfo{}, toObjectErr(erasureErr, minioMetaBucket, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||||
|
return ObjectInfo{}, toObjectErr(closeErr, minioMetaBucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||||
// than specified in request header.
|
// than specified in request header.
|
||||||
if n < data.Size() {
|
if n < data.Size() {
|
||||||
@@ -1417,11 +1427,15 @@ func (er erasureObjects) putObject(ctx context.Context, bucket string, object st
|
|||||||
bugLogIf(ctx, err)
|
bugLogIf(ctx, err)
|
||||||
}
|
}
|
||||||
n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
n, erasureErr := erasure.Encode(ctx, toEncode, writers, buffer, writeQuorum)
|
||||||
closeBitrotWriters(writers)
|
closeErrs := closeBitrotWriters(writers)
|
||||||
if erasureErr != nil {
|
if erasureErr != nil {
|
||||||
return ObjectInfo{}, toObjectErr(erasureErr, bucket, object)
|
return ObjectInfo{}, toObjectErr(erasureErr, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if closeErr := reduceWriteQuorumErrs(ctx, closeErrs, objectOpIgnoredErrs, writeQuorum); closeErr != nil {
|
||||||
|
return ObjectInfo{}, toObjectErr(closeErr, bucket, object)
|
||||||
|
}
|
||||||
|
|
||||||
// Should return IncompleteBody{} error when reader has fewer bytes
|
// Should return IncompleteBody{} error when reader has fewer bytes
|
||||||
// than specified in request header.
|
// than specified in request header.
|
||||||
if n < data.Size() {
|
if n < data.Size() {
|
||||||
@@ -1618,7 +1632,7 @@ func (er erasureObjects) deleteObjectVersion(ctx context.Context, bucket, object
|
|||||||
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
|
func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objects []ObjectToDelete, opts ObjectOptions) ([]DeletedObject, []error) {
|
||||||
if !opts.NoAuditLog {
|
if !opts.NoAuditLog {
|
||||||
for _, obj := range objects {
|
for _, obj := range objects {
|
||||||
auditObjectErasureSet(ctx, "DeleteObjects", obj.ObjectV.ObjectName, &er)
|
auditObjectErasureSet(ctx, "DeleteObjects", obj.ObjectName, &er)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,8 +1714,21 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
|||||||
}
|
}
|
||||||
|
|
||||||
dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
|
dedupVersions := make([]FileInfoVersions, 0, len(versionsMap))
|
||||||
for _, version := range versionsMap {
|
for _, fivs := range versionsMap {
|
||||||
dedupVersions = append(dedupVersions, version)
|
// Removal of existing versions and adding a delete marker in the same
|
||||||
|
// request is supported. At the same time, we cannot allow adding
|
||||||
|
// two delete markers on top of any object. To avoid this situation,
|
||||||
|
// we will sort deletions to execute existing deletion first,
|
||||||
|
// then add only one delete marker if requested
|
||||||
|
sort.SliceStable(fivs.Versions, func(i, j int) bool {
|
||||||
|
return !fivs.Versions[i].Deleted
|
||||||
|
})
|
||||||
|
if idx := slices.IndexFunc(fivs.Versions, func(fi FileInfo) bool {
|
||||||
|
return fi.Deleted
|
||||||
|
}); idx > -1 {
|
||||||
|
fivs.Versions = fivs.Versions[:idx+1]
|
||||||
|
}
|
||||||
|
dedupVersions = append(dedupVersions, fivs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize list of errors.
|
// Initialize list of errors.
|
||||||
@@ -1726,12 +1753,6 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, v := range dedupVersions[i].Versions {
|
for _, v := range dedupVersions[i].Versions {
|
||||||
if err == errFileNotFound || err == errFileVersionNotFound {
|
|
||||||
if !dobjects[v.Idx].DeleteMarker {
|
|
||||||
// Not delete marker, if not found, ok.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delObjErrs[index][v.Idx] = err
|
delObjErrs[index][v.Idx] = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1751,6 +1772,13 @@ func (er erasureObjects) DeleteObjects(ctx context.Context, bucket string, objec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex])
|
err := reduceWriteQuorumErrs(ctx, diskErrs, objectOpIgnoredErrs, writeQuorums[objIndex])
|
||||||
|
if err == nil {
|
||||||
|
dobjects[objIndex].found = true
|
||||||
|
} else if isErrVersionNotFound(err) || isErrObjectNotFound(err) {
|
||||||
|
if !dobjects[objIndex].DeleteMarker {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if objects[objIndex].VersionID != "" {
|
if objects[objIndex].VersionID != "" {
|
||||||
errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
|
errs[objIndex] = toObjectErr(err, bucket, objects[objIndex].ObjectName, objects[objIndex].VersionID)
|
||||||
} else {
|
} else {
|
||||||
@@ -2004,7 +2032,7 @@ func (er erasureObjects) DeleteObject(ctx context.Context, bucket, object string
|
|||||||
if opts.VersionPurgeStatus().Empty() && opts.DeleteMarkerReplicationStatus().Empty() {
|
if opts.VersionPurgeStatus().Empty() && opts.DeleteMarkerReplicationStatus().Empty() {
|
||||||
markDelete = false
|
markDelete = false
|
||||||
}
|
}
|
||||||
if opts.VersionPurgeStatus() == Complete {
|
if opts.VersionPurgeStatus() == replication.VersionPurgeComplete {
|
||||||
markDelete = false
|
markDelete = false
|
||||||
}
|
}
|
||||||
// now, since VersionPurgeStatus() is already set, we can let the
|
// now, since VersionPurgeStatus() is already set, we can let the
|
||||||
@@ -2138,7 +2166,7 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
|||||||
|
|
||||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
if shouldCheckForDangling(err, errs, bucket) {
|
||||||
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
||||||
if derr == nil {
|
if derr == nil {
|
||||||
if opts.VersionID != "" {
|
if opts.VersionID != "" {
|
||||||
@@ -2192,14 +2220,16 @@ func (er erasureObjects) PutObjectMetadata(ctx context.Context, bucket, object s
|
|||||||
|
|
||||||
// PutObjectTags - replace or add tags to an existing object
|
// PutObjectTags - replace or add tags to an existing object
|
||||||
func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
|
func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object string, tags string, opts ObjectOptions) (ObjectInfo, error) {
|
||||||
// Lock the object before updating tags.
|
if !opts.NoLock {
|
||||||
lk := er.NewNSLock(bucket, object)
|
// Lock the object before updating tags.
|
||||||
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
lk := er.NewNSLock(bucket, object)
|
||||||
if err != nil {
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
return ObjectInfo{}, err
|
if err != nil {
|
||||||
|
return ObjectInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
}
|
}
|
||||||
ctx = lkctx.Context()
|
|
||||||
defer lk.Unlock(lkctx)
|
|
||||||
|
|
||||||
disks := er.getDisks()
|
disks := er.getDisks()
|
||||||
|
|
||||||
@@ -2215,7 +2245,7 @@ func (er erasureObjects) PutObjectTags(ctx context.Context, bucket, object strin
|
|||||||
|
|
||||||
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
readQuorum, _, err := objectQuorumFromMeta(ctx, metaArr, errs, er.defaultParityCount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errErasureReadQuorum) && !strings.HasPrefix(bucket, minioMetaBucket) {
|
if shouldCheckForDangling(err, errs, bucket) {
|
||||||
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
_, derr := er.deleteIfDangling(context.Background(), bucket, object, metaArr, errs, nil, opts)
|
||||||
if derr == nil {
|
if derr == nil {
|
||||||
if opts.VersionID != "" {
|
if opts.VersionID != "" {
|
||||||
@@ -2310,14 +2340,16 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire write lock before starting to transition the object.
|
if !opts.NoLock {
|
||||||
lk := er.NewNSLock(bucket, object)
|
// Acquire write lock before starting to transition the object.
|
||||||
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
lk := er.NewNSLock(bucket, object)
|
||||||
if err != nil {
|
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
}
|
}
|
||||||
ctx = lkctx.Context()
|
|
||||||
defer lk.Unlock(lkctx)
|
|
||||||
|
|
||||||
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
|
fi, metaArr, onlineDisks, err := er.getObjectFileInfo(ctx, bucket, object, opts, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2355,6 +2387,7 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
|||||||
|
|
||||||
destObj, err := genTransitionObjName(bucket)
|
destObj, err := genTransitionObjName(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
traceFn(ILMTransition, nil, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2365,9 +2398,14 @@ func (er erasureObjects) TransitionObject(ctx context.Context, bucket, object st
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var rv remoteVersionID
|
var rv remoteVersionID
|
||||||
rv, err = tgtClient.Put(ctx, destObj, pr, fi.Size)
|
rv, err = tgtClient.PutWithMeta(ctx, destObj, pr, fi.Size, map[string]string{
|
||||||
|
"name": object, // preserve the original name of the object on the remote tier object metadata.
|
||||||
|
// this is just for future reverse lookup() purposes (applies only for new objects)
|
||||||
|
// does not apply retro-actively on already transitioned objects.
|
||||||
|
})
|
||||||
pr.CloseWithError(err)
|
pr.CloseWithError(err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
traceFn(ILMTransition, nil, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fi.TransitionStatus = lifecycle.TransitionComplete
|
fi.TransitionStatus = lifecycle.TransitionComplete
|
||||||
|
|||||||
@@ -131,6 +131,75 @@ func TestErasureDeleteObjectBasic(t *testing.T) {
|
|||||||
removeRoots(fsDirs)
|
removeRoots(fsDirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteObjectsVersionedTwoPools(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
obj, fsDirs, err := prepareErasurePools()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to initialize 'Erasure' object layer.", err)
|
||||||
|
}
|
||||||
|
// Remove all dirs.
|
||||||
|
for _, dir := range fsDirs {
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketName := "bucket"
|
||||||
|
objectName := "myobject"
|
||||||
|
err = obj.MakeBucket(ctx, bucketName, MakeBucketOptions{
|
||||||
|
VersioningEnabled: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
z, ok := obj.(*erasureServerPools)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unexpected object layer type")
|
||||||
|
}
|
||||||
|
|
||||||
|
versions := make([]string, 2)
|
||||||
|
for i := range z.serverPools {
|
||||||
|
objInfo, err := z.serverPools[i].PutObject(ctx, bucketName, objectName,
|
||||||
|
mustGetPutObjReader(t, bytes.NewReader([]byte("abcd")), int64(len("abcd")), "", ""), ObjectOptions{
|
||||||
|
Versioned: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Erasure Object upload failed: <ERROR> %s", err)
|
||||||
|
}
|
||||||
|
versions[i] = objInfo.VersionID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove and check the version in the second pool, then
|
||||||
|
// remove and check the version in the first pool
|
||||||
|
for testIdx, vid := range []string{versions[1], versions[0]} {
|
||||||
|
names := []ObjectToDelete{
|
||||||
|
{
|
||||||
|
ObjectV: ObjectV{
|
||||||
|
ObjectName: objectName,
|
||||||
|
VersionID: vid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, delErrs := obj.DeleteObjects(ctx, bucketName, names, ObjectOptions{
|
||||||
|
Versioned: true,
|
||||||
|
})
|
||||||
|
for i := range delErrs {
|
||||||
|
if delErrs[i] != nil {
|
||||||
|
t.Errorf("Test %d: Failed to remove object `%v` with the error: `%v`", testIdx, names[i], delErrs[i])
|
||||||
|
}
|
||||||
|
_, statErr := obj.GetObjectInfo(ctx, bucketName, objectName, ObjectOptions{
|
||||||
|
VersionID: names[i].VersionID,
|
||||||
|
})
|
||||||
|
switch statErr.(type) {
|
||||||
|
case VersionNotFound:
|
||||||
|
default:
|
||||||
|
t.Errorf("Test %d: Object %s is not removed", testIdx, objectName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeleteObjectsVersioned(t *testing.T) {
|
func TestDeleteObjectsVersioned(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -177,7 +246,6 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
|||||||
VersionID: objInfo.VersionID,
|
VersionID: objInfo.VersionID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
names = append(names, ObjectToDelete{
|
names = append(names, ObjectToDelete{
|
||||||
ObjectV: ObjectV{
|
ObjectV: ObjectV{
|
||||||
@@ -197,7 +265,7 @@ func TestDeleteObjectsVersioned(t *testing.T) {
|
|||||||
|
|
||||||
for i, test := range testCases {
|
for i, test := range testCases {
|
||||||
_, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{
|
_, statErr := obj.GetObjectInfo(ctx, test.bucket, test.object, ObjectOptions{
|
||||||
VersionID: names[i].ObjectV.VersionID,
|
VersionID: names[i].VersionID,
|
||||||
})
|
})
|
||||||
switch statErr.(type) {
|
switch statErr.(type) {
|
||||||
case VersionNotFound:
|
case VersionNotFound:
|
||||||
|
|||||||
@@ -845,7 +845,7 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
// to decommission, just skip it, this also includes
|
// to decommission, just skip it, this also includes
|
||||||
// any other versions that have already expired.
|
// any other versions that have already expired.
|
||||||
remainingVersions := len(fivs.Versions) - expired
|
remainingVersions := len(fivs.Versions) - expired
|
||||||
if version.Deleted && remainingVersions == 1 {
|
if version.Deleted && remainingVersions == 1 && rcfg == nil {
|
||||||
decommissioned++
|
decommissioned++
|
||||||
stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
stopFn(version.Size, errors.New("DELETE marked object with no other non-current versions will be skipped"))
|
||||||
continue
|
continue
|
||||||
@@ -1014,18 +1014,14 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool
|
|||||||
defer wk.Give()
|
defer wk.Give()
|
||||||
// We will perpetually retry listing if it fails, since we cannot
|
// We will perpetually retry listing if it fails, since we cannot
|
||||||
// possibly give up in this matter
|
// possibly give up in this matter
|
||||||
for {
|
for !contextCanceled(ctx) {
|
||||||
if contextCanceled(ctx) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
err := set.listObjectsToDecommission(ctx, bi,
|
err := set.listObjectsToDecommission(ctx, bi,
|
||||||
func(entry metaCacheEntry) {
|
func(entry metaCacheEntry) {
|
||||||
wk.Take()
|
wk.Take()
|
||||||
go decommissionEntry(entry)
|
go decommissionEntry(entry)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err == nil || errors.Is(err, context.Canceled) {
|
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, errVolumeNotFound) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
setN := humanize.Ordinal(setIdx + 1)
|
setN := humanize.Ordinal(setIdx + 1)
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ func prepareErasurePools() (ObjectLayer, []string, error) {
|
|||||||
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
pools := mustGetPoolEndpoints(0, fsDirs[:16]...)
|
||||||
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
pools = append(pools, mustGetPoolEndpoints(1, fsDirs[16:]...)...)
|
||||||
|
|
||||||
// Everything is fine, should return nil
|
objLayer, _, err := initObjectLayer(context.Background(), pools)
|
||||||
objLayer, err := newErasureServerPools(context.Background(), pools)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
removeRoots(fsDirs)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return objLayer, fsDirs, nil
|
return objLayer, fsDirs, nil
|
||||||
|
|||||||
+140
-82
@@ -256,7 +256,7 @@ func (z *erasureServerPools) NewNSLock(bucket string, objects ...string) RWLocke
|
|||||||
|
|
||||||
// GetDisksID will return disks by their ID.
|
// GetDisksID will return disks by their ID.
|
||||||
func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
|
func (z *erasureServerPools) GetDisksID(ids ...string) []StorageAPI {
|
||||||
idMap := make(map[string]struct{})
|
idMap := make(map[string]struct{}, len(ids))
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
idMap[id] = struct{}{}
|
idMap[id] = struct{}{}
|
||||||
}
|
}
|
||||||
@@ -357,7 +357,7 @@ func (p serverPoolsAvailableSpace) TotalAvailable() uint64 {
|
|||||||
|
|
||||||
// FilterMaxUsed will filter out any pools that has used percent bigger than max,
|
// FilterMaxUsed will filter out any pools that has used percent bigger than max,
|
||||||
// unless all have that, in which case all are preserved.
|
// unless all have that, in which case all are preserved.
|
||||||
func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
func (p serverPoolsAvailableSpace) FilterMaxUsed(maxUsed int) {
|
||||||
// We aren't modifying p, only entries in it, so we don't need to receive a pointer.
|
// We aren't modifying p, only entries in it, so we don't need to receive a pointer.
|
||||||
if len(p) <= 1 {
|
if len(p) <= 1 {
|
||||||
// Nothing to do.
|
// Nothing to do.
|
||||||
@@ -365,7 +365,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
|||||||
}
|
}
|
||||||
var ok bool
|
var ok bool
|
||||||
for _, z := range p {
|
for _, z := range p {
|
||||||
if z.Available > 0 && z.MaxUsedPct < max {
|
if z.Available > 0 && z.MaxUsedPct < maxUsed {
|
||||||
ok = true
|
ok = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ func (p serverPoolsAvailableSpace) FilterMaxUsed(max int) {
|
|||||||
|
|
||||||
// Remove entries that are above.
|
// Remove entries that are above.
|
||||||
for i, z := range p {
|
for i, z := range p {
|
||||||
if z.Available > 0 && z.MaxUsedPct < max {
|
if z.Available > 0 && z.MaxUsedPct < maxUsed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p[i].Available = 0
|
p[i].Available = 0
|
||||||
@@ -1248,89 +1248,42 @@ func (z *erasureServerPools) DeleteObjects(ctx context.Context, bucket string, o
|
|||||||
ctx = lkctx.Context()
|
ctx = lkctx.Context()
|
||||||
defer multiDeleteLock.Unlock(lkctx)
|
defer multiDeleteLock.Unlock(lkctx)
|
||||||
|
|
||||||
// Fetch location of up to 10 objects concurrently.
|
dObjectsByPool := make([][]DeletedObject, len(z.serverPools))
|
||||||
poolObjIdxMap := map[int][]ObjectToDelete{}
|
dErrsByPool := make([][]error, len(z.serverPools))
|
||||||
origIndexMap := map[int][]int{}
|
|
||||||
|
|
||||||
// Always perform 1/10th of the number of objects per delete
|
eg := errgroup.WithNErrs(len(z.serverPools)).WithConcurrency(len(z.serverPools))
|
||||||
concurrent := len(objects) / 10
|
for i, pool := range z.serverPools {
|
||||||
if concurrent <= 10 {
|
i := i
|
||||||
// if we cannot get 1/10th then choose the number of
|
pool := pool
|
||||||
// objects as concurrent.
|
|
||||||
concurrent = len(objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
eg := errgroup.WithNErrs(len(objects)).WithConcurrency(concurrent)
|
|
||||||
for j, obj := range objects {
|
|
||||||
j := j
|
|
||||||
obj := obj
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
pinfo, _, err := z.getPoolInfoExistingWithOpts(ctx, bucket, obj.ObjectName, ObjectOptions{
|
dObjectsByPool[i], dErrsByPool[i] = pool.DeleteObjects(ctx, bucket, objects, opts)
|
||||||
NoLock: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if !isErrObjectNotFound(err) && !isErrVersionNotFound(err) {
|
|
||||||
derrs[j] = err
|
|
||||||
}
|
|
||||||
dobjects[j] = DeletedObject{
|
|
||||||
ObjectName: decodeDirObject(obj.ObjectName),
|
|
||||||
VersionID: obj.VersionID,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete marker already present we are not going to create new delete markers.
|
|
||||||
if pinfo.ObjInfo.DeleteMarker && obj.VersionID == "" {
|
|
||||||
dobjects[j] = DeletedObject{
|
|
||||||
DeleteMarker: pinfo.ObjInfo.DeleteMarker,
|
|
||||||
DeleteMarkerVersionID: pinfo.ObjInfo.VersionID,
|
|
||||||
DeleteMarkerMTime: DeleteMarkerMTime{pinfo.ObjInfo.ModTime},
|
|
||||||
ObjectName: decodeDirObject(pinfo.ObjInfo.Name),
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := pinfo.Index
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
poolObjIdxMap[idx] = append(poolObjIdxMap[idx], obj)
|
|
||||||
origIndexMap[idx] = append(origIndexMap[idx], j)
|
|
||||||
return nil
|
return nil
|
||||||
}, j)
|
}, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
eg.Wait() // wait to check all the pools.
|
eg.Wait() // wait to check all the pools.
|
||||||
|
|
||||||
if len(poolObjIdxMap) > 0 {
|
for i := range dobjects {
|
||||||
// Delete concurrently in all server pools.
|
// Iterate over pools
|
||||||
var wg sync.WaitGroup
|
for pool := range z.serverPools {
|
||||||
wg.Add(len(z.serverPools))
|
if dErrsByPool[pool][i] == nil && dObjectsByPool[pool][i].found {
|
||||||
for idx, pool := range z.serverPools {
|
// A fast exit when the object is found and removed
|
||||||
go func(idx int, pool *erasureSets) {
|
dobjects[i] = dObjectsByPool[pool][i]
|
||||||
defer wg.Done()
|
derrs[i] = nil
|
||||||
objs := poolObjIdxMap[idx]
|
break
|
||||||
if len(objs) == 0 {
|
}
|
||||||
return
|
if derrs[i] == nil {
|
||||||
}
|
// No error related to this object is found, assign this pool result
|
||||||
orgIndexes := origIndexMap[idx]
|
// whether it is nil because there is no object found or because of
|
||||||
deletedObjects, errs := pool.DeleteObjects(ctx, bucket, objs, opts)
|
// some other errors such erasure quorum errors.
|
||||||
mu.Lock()
|
dobjects[i] = dObjectsByPool[pool][i]
|
||||||
for i, derr := range errs {
|
derrs[i] = dErrsByPool[pool][i]
|
||||||
if derr != nil {
|
}
|
||||||
derrs[orgIndexes[i]] = derr
|
|
||||||
}
|
|
||||||
deletedObjects[i].ObjectName = decodeDirObject(deletedObjects[i].ObjectName)
|
|
||||||
dobjects[orgIndexes[i]] = deletedObjects[i]
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}(idx, pool)
|
|
||||||
}
|
}
|
||||||
wg.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range dobjects {
|
||||||
|
dobjects[i].ObjectName = decodeDirObject(dobjects[i].ObjectName)
|
||||||
|
}
|
||||||
return dobjects, derrs
|
return dobjects, derrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1890,9 +1843,18 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
|||||||
return MultipartInfo{}, err
|
return MultipartInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
|
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return MultipartInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer uploadIDLock.RUnlock(lkctx)
|
||||||
|
|
||||||
if z.SinglePool() {
|
if z.SinglePool() {
|
||||||
return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
return z.serverPools[0].GetMultipartInfo(ctx, bucket, object, uploadID, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, pool := range z.serverPools {
|
for idx, pool := range z.serverPools {
|
||||||
if z.IsSuspended(idx) {
|
if z.IsSuspended(idx) {
|
||||||
continue
|
continue
|
||||||
@@ -1908,6 +1870,7 @@ func (z *erasureServerPools) GetMultipartInfo(ctx context.Context, bucket, objec
|
|||||||
// any other unhandled error return right here.
|
// any other unhandled error return right here.
|
||||||
return MultipartInfo{}, err
|
return MultipartInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return MultipartInfo{}, InvalidUploadID{
|
return MultipartInfo{}, InvalidUploadID{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: object,
|
Object: object,
|
||||||
@@ -1921,9 +1884,18 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
|||||||
return ListPartsInfo{}, err
|
return ListPartsInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
|
lkctx, err := uploadIDLock.GetRLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return ListPartsInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer uploadIDLock.RUnlock(lkctx)
|
||||||
|
|
||||||
if z.SinglePool() {
|
if z.SinglePool() {
|
||||||
return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
return z.serverPools[0].ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, pool := range z.serverPools {
|
for idx, pool := range z.serverPools {
|
||||||
if z.IsSuspended(idx) {
|
if z.IsSuspended(idx) {
|
||||||
continue
|
continue
|
||||||
@@ -1937,6 +1909,7 @@ func (z *erasureServerPools) ListObjectParts(ctx context.Context, bucket, object
|
|||||||
}
|
}
|
||||||
return ListPartsInfo{}, err
|
return ListPartsInfo{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListPartsInfo{}, InvalidUploadID{
|
return ListPartsInfo{}, InvalidUploadID{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: object,
|
Object: object,
|
||||||
@@ -1957,6 +1930,14 @@ func (z *erasureServerPools) AbortMultipartUpload(ctx context.Context, bucket, o
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lk := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
|
||||||
if z.SinglePool() {
|
if z.SinglePool() {
|
||||||
return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
return z.serverPools[0].AbortMultipartUpload(ctx, bucket, object, uploadID, opts)
|
||||||
}
|
}
|
||||||
@@ -1995,6 +1976,16 @@ func (z *erasureServerPools) CompleteMultipartUpload(ctx context.Context, bucket
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Hold write locks to verify uploaded parts, also disallows any
|
||||||
|
// parallel PutObjectPart() requests.
|
||||||
|
uploadIDLock := z.NewNSLock(bucket, pathJoin(object, uploadID))
|
||||||
|
wlkctx, err := uploadIDLock.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return objInfo, err
|
||||||
|
}
|
||||||
|
ctx = wlkctx.Context()
|
||||||
|
defer uploadIDLock.Unlock(wlkctx)
|
||||||
|
|
||||||
if z.SinglePool() {
|
if z.SinglePool() {
|
||||||
return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
return z.serverPools[0].CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts)
|
||||||
}
|
}
|
||||||
@@ -2430,6 +2421,7 @@ func (z *erasureServerPools) Walk(ctx context.Context, bucket, prefix string, re
|
|||||||
// HealObjectFn closure function heals the object.
|
// HealObjectFn closure function heals the object.
|
||||||
type HealObjectFn func(bucket, object, versionID string, scanMode madmin.HealScanMode) error
|
type HealObjectFn func(bucket, object, versionID string, scanMode madmin.HealScanMode) error
|
||||||
|
|
||||||
|
// List a prefix or a single object versions and heal
|
||||||
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
|
func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix string, opts madmin.HealOpts, healObjectFn HealObjectFn) error {
|
||||||
healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error {
|
healEntry := func(bucket string, entry metaCacheEntry, scanMode madmin.HealScanMode) error {
|
||||||
if entry.isDir() {
|
if entry.isDir() {
|
||||||
@@ -2493,7 +2485,7 @@ func (z *erasureServerPools) HealObjects(ctx context.Context, bucket, prefix str
|
|||||||
go func(idx int, set *erasureObjects) {
|
go func(idx int, set *erasureObjects) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.ScanMode, healEntry)
|
errs[idx] = set.listAndHeal(ctx, bucket, prefix, opts.Recursive, opts.ScanMode, healEntry)
|
||||||
}(idx, set)
|
}(idx, set)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -2545,15 +2537,22 @@ func (z *erasureServerPools) HealObject(ctx context.Context, bucket, object, ver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr := madmin.HealResultItem{
|
||||||
|
Type: madmin.HealItemObject,
|
||||||
|
Bucket: bucket,
|
||||||
|
Object: object,
|
||||||
|
VersionID: versionID,
|
||||||
|
}
|
||||||
|
|
||||||
// At this stage, all errors are 'not found'
|
// At this stage, all errors are 'not found'
|
||||||
if versionID != "" {
|
if versionID != "" {
|
||||||
return madmin.HealResultItem{}, VersionNotFound{
|
return hr, VersionNotFound{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: object,
|
Object: object,
|
||||||
VersionID: versionID,
|
VersionID: versionID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return madmin.HealResultItem{}, ObjectNotFound{
|
return hr, ObjectNotFound{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Object: object,
|
Object: object,
|
||||||
}
|
}
|
||||||
@@ -2774,7 +2773,19 @@ func (z *erasureServerPools) PutObjectMetadata(ctx context.Context, bucket, obje
|
|||||||
return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts)
|
return z.serverPools[0].PutObjectMetadata(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.NoLock {
|
||||||
|
// Lock the object before updating metadata.
|
||||||
|
lk := z.NewNSLock(bucket, object)
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
}
|
||||||
|
|
||||||
opts.MetadataChg = true
|
opts.MetadataChg = true
|
||||||
|
opts.NoLock = true
|
||||||
// We don't know the size here set 1GiB at least.
|
// We don't know the size here set 1GiB at least.
|
||||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2791,7 +2802,19 @@ func (z *erasureServerPools) PutObjectTags(ctx context.Context, bucket, object s
|
|||||||
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
|
return z.serverPools[0].PutObjectTags(ctx, bucket, object, tags, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.NoLock {
|
||||||
|
// Lock the object before updating tags.
|
||||||
|
lk := z.NewNSLock(bucket, object)
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
}
|
||||||
|
|
||||||
opts.MetadataChg = true
|
opts.MetadataChg = true
|
||||||
|
opts.NoLock = true
|
||||||
|
|
||||||
// We don't know the size here set 1GiB at least.
|
// We don't know the size here set 1GiB at least.
|
||||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||||
@@ -2809,8 +2832,19 @@ func (z *erasureServerPools) DeleteObjectTags(ctx context.Context, bucket, objec
|
|||||||
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
|
return z.serverPools[0].DeleteObjectTags(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.MetadataChg = true
|
if !opts.NoLock {
|
||||||
|
// Lock the object before deleting tags.
|
||||||
|
lk := z.NewNSLock(bucket, object)
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return ObjectInfo{}, err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.MetadataChg = true
|
||||||
|
opts.NoLock = true
|
||||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ObjectInfo{}, err
|
return ObjectInfo{}, err
|
||||||
@@ -2841,8 +2875,20 @@ func (z *erasureServerPools) TransitionObject(ctx context.Context, bucket, objec
|
|||||||
return z.serverPools[0].TransitionObject(ctx, bucket, object, opts)
|
return z.serverPools[0].TransitionObject(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.NoLock {
|
||||||
|
// Acquire write lock before starting to transition the object.
|
||||||
|
lk := z.NewNSLock(bucket, object)
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid transitioning an object from a pool being decommissioned.
|
// Avoid transitioning an object from a pool being decommissioned.
|
||||||
opts.SkipDecommissioned = true
|
opts.SkipDecommissioned = true
|
||||||
|
opts.NoLock = true
|
||||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2858,8 +2904,20 @@ func (z *erasureServerPools) RestoreTransitionedObject(ctx context.Context, buck
|
|||||||
return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts)
|
return z.serverPools[0].RestoreTransitionedObject(ctx, bucket, object, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !opts.NoLock {
|
||||||
|
// Acquire write lock before restoring transitioned object
|
||||||
|
lk := z.NewNSLock(bucket, object)
|
||||||
|
lkctx, err := lk.GetLock(ctx, globalDeleteOperationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx = lkctx.Context()
|
||||||
|
defer lk.Unlock(lkctx)
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid restoring object from a pool being decommissioned.
|
// Avoid restoring object from a pool being decommissioned.
|
||||||
opts.SkipDecommissioned = true
|
opts.SkipDecommissioned = true
|
||||||
|
opts.NoLock = true
|
||||||
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
idx, err := z.getPoolIdxExistingWithOpts(ctx, bucket, object, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+4
-11
@@ -39,6 +39,7 @@ import (
|
|||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v3/console"
|
"github.com/minio/pkg/v3/console"
|
||||||
"github.com/minio/pkg/v3/sync/errgroup"
|
"github.com/minio/pkg/v3/sync/errgroup"
|
||||||
|
"github.com/puzpuzpuz/xsync/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setsDsyncLockers is encapsulated type for Close()
|
// setsDsyncLockers is encapsulated type for Close()
|
||||||
@@ -570,10 +571,7 @@ func auditObjectErasureSet(ctx context.Context, api, object string, set *erasure
|
|||||||
|
|
||||||
// NewNSLock - initialize a new namespace RWLocker instance.
|
// NewNSLock - initialize a new namespace RWLocker instance.
|
||||||
func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker {
|
func (s *erasureSets) NewNSLock(bucket string, objects ...string) RWLocker {
|
||||||
if len(objects) == 1 {
|
return s.sets[0].NewNSLock(bucket, objects...)
|
||||||
return s.getHashedSet(objects[0]).NewNSLock(bucket, objects...)
|
|
||||||
}
|
|
||||||
return s.getHashedSet("").NewNSLock(bucket, objects...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDriveCount returns the current drives per set.
|
// SetDriveCount returns the current drives per set.
|
||||||
@@ -704,9 +702,8 @@ func (s *erasureSets) getHashedSet(input string) (set *erasureObjects) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listDeletedBuckets lists deleted buckets from all disks.
|
// listDeletedBuckets lists deleted buckets from all disks.
|
||||||
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets map[string]VolInfo, readQuorum int) error {
|
func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBuckets *xsync.MapOf[string, VolInfo], readQuorum int) error {
|
||||||
g := errgroup.WithNErrs(len(storageDisks))
|
g := errgroup.WithNErrs(len(storageDisks))
|
||||||
var mu sync.Mutex
|
|
||||||
for index := range storageDisks {
|
for index := range storageDisks {
|
||||||
index := index
|
index := index
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
@@ -725,11 +722,7 @@ func listDeletedBuckets(ctx context.Context, storageDisks []StorageAPI, delBucke
|
|||||||
vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
|
vi, err := storageDisks[index].StatVol(ctx, pathJoin(minioMetaBucket, bucketMetaPrefix, deletedBucketsPrefix, volName))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vi.Name = strings.TrimSuffix(volName, SlashSeparator)
|
vi.Name = strings.TrimSuffix(volName, SlashSeparator)
|
||||||
mu.Lock()
|
delBuckets.Store(volName, vi)
|
||||||
if _, ok := delBuckets[volName]; !ok {
|
|
||||||
delBuckets[volName] = vi
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -551,9 +551,6 @@ func (er erasureObjects) nsScanner(ctx context.Context, buckets []BucketInfo, wa
|
|||||||
var root dataUsageEntry
|
var root dataUsageEntry
|
||||||
if r := cache.root(); r != nil {
|
if r := cache.root(); r != nil {
|
||||||
root = cache.flatten(*r)
|
root = cache.flatten(*r)
|
||||||
if root.ReplicationStats.empty() {
|
|
||||||
root.ReplicationStats = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -286,6 +288,10 @@ func (driver *ftpDriver) CheckPasswd(c *ftp.Context, username, password string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
|
func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error) {
|
||||||
|
tr := http.RoundTripper(globalRemoteFTPClientTransport)
|
||||||
|
if host, _, err := net.SplitHostPort(ctx.Sess.RemoteAddr().String()); err == nil {
|
||||||
|
tr = forwardForTransport{tr: tr, fwd: host}
|
||||||
|
}
|
||||||
ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
|
ui, ok := globalIAMSys.GetUser(context.Background(), ctx.Sess.LoginUser())
|
||||||
if !ok && !globalIAMSys.LDAPConfig.Enabled() {
|
if !ok && !globalIAMSys.LDAPConfig.Enabled() {
|
||||||
return nil, errNoSuchUser
|
return nil, errNoSuchUser
|
||||||
@@ -361,9 +367,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return minio.New(driver.endpoint, &minio.Options{
|
return minio.New(driver.endpoint, &minio.Options{
|
||||||
Creds: mcreds,
|
Creds: mcreds,
|
||||||
Secure: globalIsTLS,
|
Secure: globalIsTLS,
|
||||||
Transport: globalRemoteFTPClientTransport,
|
Transport: tr,
|
||||||
|
TrailingHeaders: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,9 +382,10 @@ func (driver *ftpDriver) getMinIOClient(ctx *ftp.Context) (*minio.Client, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return minio.New(driver.endpoint, &minio.Options{
|
return minio.New(driver.endpoint, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
Creds: credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""),
|
||||||
Secure: globalIsTLS,
|
Secure: globalIsTLS,
|
||||||
Transport: globalRemoteFTPClientTransport,
|
Transport: tr,
|
||||||
|
TrailingHeaders: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +554,7 @@ func (driver *ftpDriver) PutFile(ctx *ftp.Context, objPath string, data io.Reade
|
|||||||
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
|
info, err := clnt.PutObject(context.Background(), bucket, object, data, -1, minio.PutObjectOptions{
|
||||||
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
ContentType: mimedb.TypeByExtension(path.Ext(object)),
|
||||||
DisableContentSha256: true,
|
DisableContentSha256: true,
|
||||||
|
Checksum: minio.ChecksumFullObjectCRC32C,
|
||||||
})
|
})
|
||||||
n = info.Size
|
n = info.Size
|
||||||
return n, err
|
return n, err
|
||||||
|
|||||||
+32
-20
@@ -22,19 +22,17 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/minio-go/v7/pkg/s3utils"
|
"github.com/minio/minio-go/v7/pkg/s3utils"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
"github.com/minio/minio/internal/grid"
|
"github.com/minio/minio/internal/grid"
|
||||||
xnet "github.com/minio/pkg/v3/net"
|
xnet "github.com/minio/pkg/v3/net"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
"golang.org/x/exp/slices"
|
|
||||||
|
|
||||||
"github.com/minio/minio/internal/amztime"
|
"github.com/minio/minio/internal/amztime"
|
||||||
"github.com/minio/minio/internal/config/dns"
|
"github.com/minio/minio/internal/config/dns"
|
||||||
@@ -75,7 +73,7 @@ const (
|
|||||||
// and must not set by clients
|
// and must not set by clients
|
||||||
func containsReservedMetadata(header http.Header) bool {
|
func containsReservedMetadata(header http.Header) bool {
|
||||||
for key := range header {
|
for key := range header {
|
||||||
if slices.Contains(maps.Keys(validSSEReplicationHeaders), key) {
|
if _, ok := validSSEReplicationHeaders[key]; ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
|
if stringsHasPrefixFold(key, ReservedMetadataPrefix) {
|
||||||
@@ -295,12 +293,6 @@ func parseAmzDateHeader(req *http.Request) (time.Time, APIErrorCode) {
|
|||||||
return time.Time{}, ErrMissingDateHeader
|
return time.Time{}, ErrMissingDateHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bad path components to be rejected by the path validity handler.
|
|
||||||
const (
|
|
||||||
dotdotComponent = ".."
|
|
||||||
dotComponent = "."
|
|
||||||
)
|
|
||||||
|
|
||||||
func hasBadHost(host string) error {
|
func hasBadHost(host string) error {
|
||||||
if globalIsCICD && strings.TrimSpace(host) == "" {
|
if globalIsCICD && strings.TrimSpace(host) == "" {
|
||||||
// under CI/CD test setups ignore empty hosts as invalid hosts
|
// under CI/CD test setups ignore empty hosts as invalid hosts
|
||||||
@@ -313,21 +305,41 @@ func hasBadHost(host string) error {
|
|||||||
// Check if the incoming path has bad path components,
|
// Check if the incoming path has bad path components,
|
||||||
// such as ".." and "."
|
// such as ".." and "."
|
||||||
func hasBadPathComponent(path string) bool {
|
func hasBadPathComponent(path string) bool {
|
||||||
if len(path) > 4096 {
|
n := len(path)
|
||||||
// path cannot be greater than Linux PATH_MAX
|
if n > 32<<10 {
|
||||||
// this is to avoid a busy loop, that can happen
|
// At 32K we are beyond reasonable.
|
||||||
// if the caller sends path of following style
|
|
||||||
// a/a/a/a/a/a/a/a...
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
path = filepath.ToSlash(strings.TrimSpace(path)) // For windows '\' must be converted to '/'
|
i := 0
|
||||||
for _, p := range strings.Split(path, SlashSeparator) {
|
// Skip leading slashes (for sake of Windows \ is included as well)
|
||||||
switch strings.TrimSpace(p) {
|
for i < n && (path[i] == SlashSeparatorChar || path[i] == '\\') {
|
||||||
case dotdotComponent:
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
for i < n {
|
||||||
|
// Find the next segment
|
||||||
|
start := i
|
||||||
|
for i < n && path[i] != SlashSeparatorChar && path[i] != '\\' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace of segment
|
||||||
|
segmentStart, segmentEnd := start, i
|
||||||
|
for segmentStart < segmentEnd && unicode.IsSpace(rune(path[segmentStart])) {
|
||||||
|
segmentStart++
|
||||||
|
}
|
||||||
|
for segmentEnd > segmentStart && unicode.IsSpace(rune(path[segmentEnd-1])) {
|
||||||
|
segmentEnd--
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ".." or "."
|
||||||
|
switch {
|
||||||
|
case segmentEnd-segmentStart == 2 && path[segmentStart] == '.' && path[segmentStart+1] == '.':
|
||||||
return true
|
return true
|
||||||
case dotComponent:
|
case segmentEnd-segmentStart == 1 && path[segmentStart] == '.':
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/crypto"
|
"github.com/minio/minio/internal/crypto"
|
||||||
@@ -184,3 +185,27 @@ func TestSSETLSHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Benchmark_hasBadPathComponent(t *testing.B) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "empty", input: "", want: false},
|
||||||
|
{name: "backslashes", input: `\a\a\ \\ \\\\\\\`, want: false},
|
||||||
|
{name: "long", input: strings.Repeat("a/", 2000), want: false},
|
||||||
|
{name: "long-fail", input: strings.Repeat("a/", 2000) + "../..", want: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(tt.input)))
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if got := hasBadPathComponent(tt.input); got != tt.want {
|
||||||
|
t.Fatalf("hasBadPathComponent() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+9
-8
@@ -459,8 +459,6 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var versionHealed bool
|
|
||||||
|
|
||||||
res, err := er.HealObject(ctx, bucket, encodedEntryName,
|
res, err := er.HealObject(ctx, bucket, encodedEntryName,
|
||||||
version.VersionID, madmin.HealOpts{
|
version.VersionID, madmin.HealOpts{
|
||||||
ScanMode: scanMode,
|
ScanMode: scanMode,
|
||||||
@@ -475,21 +473,23 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Look for the healing results
|
// Look for the healing results
|
||||||
if res.After.Drives[tracker.DiskIndex].State == madmin.DriveStateOk {
|
if res.After.Drives[tracker.DiskIndex].State != madmin.DriveStateOk {
|
||||||
versionHealed = true
|
err = fmt.Errorf("unexpected after heal state: %s", res.After.Drives[tracker.DiskIndex].State)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if versionHealed {
|
if err == nil {
|
||||||
bgSeq.countHealed(madmin.HealItemObject)
|
bgSeq.countHealed(madmin.HealItemObject)
|
||||||
result = healEntrySuccess(uint64(version.Size))
|
result = healEntrySuccess(uint64(version.Size))
|
||||||
} else {
|
} else {
|
||||||
bgSeq.countFailed(madmin.HealItemObject)
|
bgSeq.countFailed(madmin.HealItemObject)
|
||||||
result = healEntryFailure(uint64(version.Size))
|
result = healEntryFailure(uint64(version.Size))
|
||||||
if version.VersionID != "" {
|
if version.VersionID != "" {
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s-v(%s): %w", bucket, version.Name, version.VersionID, err))
|
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s (version-id=%s): %w",
|
||||||
|
bucket, version.Name, version.VersionID, err))
|
||||||
} else {
|
} else {
|
||||||
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w", bucket, version.Name, err))
|
healingLogIf(ctx, fmt.Errorf("unable to heal object %s/%s: %w",
|
||||||
|
bucket, version.Name, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +539,8 @@ func (er *erasureObjects) healErasureSet(ctx context.Context, buckets []string,
|
|||||||
go healEntry(bucket, *entry)
|
go healEntry(bucket, *entry)
|
||||||
},
|
},
|
||||||
finished: func(errs []error) {
|
finished: func(errs []error) {
|
||||||
if countErrs(errs, nil) != len(errs) {
|
success := countErrs(errs, nil)
|
||||||
|
if success < expectedDisks {
|
||||||
retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...))
|
retErr = fmt.Errorf("one or more errors reported during listing: %v", errors.Join(errs...))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+3
-6
@@ -28,6 +28,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
|
||||||
"github.com/minio/minio/internal/config/api"
|
"github.com/minio/minio/internal/config/api"
|
||||||
@@ -61,7 +62,6 @@ type apiConfig struct {
|
|||||||
const (
|
const (
|
||||||
cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
cgroupV1MemLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
||||||
cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max"
|
cgroupV2MemLimitFile = "/sys/fs/cgroup/memory.max"
|
||||||
cgroupMemNoLimit = 9223372036854771712
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func cgroupMemLimit() (limit uint64) {
|
func cgroupMemLimit() (limit uint64) {
|
||||||
@@ -78,10 +78,8 @@ func cgroupMemLimit() (limit uint64) {
|
|||||||
// but still, no need to interpret more
|
// but still, no need to interpret more
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if limit == cgroupMemNoLimit {
|
if limit >= 100*humanize.TiByte {
|
||||||
// No limit set, It's the highest positive signed 64-bit
|
// No limit set, or unreasonably high. Ignore
|
||||||
// integer (2^63-1), rounded down to multiples of 4096 (2^12),
|
|
||||||
// the most common page size on x86 systems - for cgroup_limits.
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return limit
|
return limit
|
||||||
@@ -368,7 +366,6 @@ func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
|||||||
writeErrorResponse(ctx, w,
|
writeErrorResponse(ctx, w,
|
||||||
errorCodes.ToAPIErr(ErrTooManyRequests),
|
errorCodes.ToAPIErr(ErrTooManyRequests),
|
||||||
r.URL)
|
r.URL)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ func getHostName(r *http.Request) (hostName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Proxy any request to an endpoint.
|
// Proxy any request to an endpoint.
|
||||||
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint) (success bool) {
|
func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, ep ProxyEndpoint, returnErr bool) (success bool) {
|
||||||
success = true
|
success = true
|
||||||
|
|
||||||
// Make sure we remove any existing headers before
|
// Make sure we remove any existing headers before
|
||||||
@@ -462,7 +462,10 @@ func proxyRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, e
|
|||||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
success = false
|
success = false
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
replLogIf(GlobalContext, err)
|
proxyLogIf(GlobalContext, err)
|
||||||
|
}
|
||||||
|
if returnErr {
|
||||||
|
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
+2
-2
@@ -133,7 +133,7 @@ func (bh *bucketHTTPStats) updateHTTPStats(bucket, api string, w *xhttp.Response
|
|||||||
bucketHTTPRequestsDuration.With(prometheus.Labels{
|
bucketHTTPRequestsDuration.With(prometheus.Labels{
|
||||||
"api": api,
|
"api": api,
|
||||||
"bucket": bucket,
|
"bucket": bucket,
|
||||||
}).Observe(w.TimeToFirstByte.Seconds())
|
}).Observe(w.TTFB().Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
bh.Lock()
|
bh.Lock()
|
||||||
@@ -433,7 +433,7 @@ func (st *HTTPStats) updateStats(api string, w *xhttp.ResponseRecorder) {
|
|||||||
st.totalS3Requests.Inc(api)
|
st.totalS3Requests.Inc(api)
|
||||||
|
|
||||||
// Increment the prometheus http request response histogram with appropriate label
|
// Increment the prometheus http request response histogram with appropriate label
|
||||||
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TimeToFirstByte.Seconds())
|
httpRequestsDuration.With(prometheus.Labels{"api": api}).Observe(w.TTFB().Seconds())
|
||||||
|
|
||||||
code := w.StatusCode
|
code := w.StatusCode
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -164,7 +164,7 @@ func httpTracerMiddleware(h http.Handler) http.Handler {
|
|||||||
Latency: reqEndTime.Sub(respRecorder.StartTime),
|
Latency: reqEndTime.Sub(respRecorder.StartTime),
|
||||||
InputBytes: inputBytes,
|
InputBytes: inputBytes,
|
||||||
OutputBytes: respRecorder.Size(),
|
OutputBytes: respRecorder.Size(),
|
||||||
TimeToFirstByte: respRecorder.TimeToFirstByte,
|
TimeToFirstByte: respRecorder.TTFB(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -493,7 +493,6 @@ func (ies *IAMEtcdStore) watch(ctx context.Context, keyPath string) <-chan iamWa
|
|||||||
keyPath: string(event.Kv.Key),
|
keyPath: string(event.Kv.Key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user